1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package io.wcm.caconfig.extensions.persistence.impl;
21
22 import static com.day.cq.commons.jcr.JcrConstants.NT_UNSTRUCTURED;
23 import static io.wcm.caconfig.extensions.persistence.impl.PersistenceUtils.commit;
24 import static io.wcm.caconfig.extensions.persistence.impl.PersistenceUtils.deleteChildrenNotInCollection;
25 import static io.wcm.caconfig.extensions.persistence.impl.PersistenceUtils.deletePageOrResource;
26 import static io.wcm.caconfig.extensions.persistence.impl.PersistenceUtils.ensureContainingPage;
27 import static io.wcm.caconfig.extensions.persistence.impl.PersistenceUtils.getOrCreateResource;
28 import static io.wcm.caconfig.extensions.persistence.impl.PersistenceUtils.replaceProperties;
29 import static io.wcm.caconfig.extensions.persistence.impl.PersistenceUtils.updatePageLastMod;
30
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Iterator;
34 import java.util.LinkedHashMap;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.regex.Pattern;
38
39 import org.apache.commons.collections4.IteratorUtils;
40 import org.apache.commons.collections4.PredicateUtils;
41 import org.apache.commons.collections4.iterators.FilterIterator;
42 import org.apache.commons.collections4.iterators.TransformIterator;
43 import org.apache.commons.lang3.StringUtils;
44 import org.apache.sling.api.resource.Resource;
45 import org.apache.sling.api.resource.ResourceResolver;
46 import org.apache.sling.api.resource.ResourceUtil;
47 import org.apache.sling.api.resource.ValueMap;
48 import org.apache.sling.caconfig.management.ConfigurationManagementSettings;
49 import org.apache.sling.caconfig.management.multiplexer.ContextPathStrategyMultiplexer;
50 import org.apache.sling.caconfig.resource.spi.ConfigurationResourceResolvingStrategy;
51 import org.apache.sling.caconfig.resource.spi.ContextResource;
52 import org.apache.sling.caconfig.spi.ConfigurationCollectionPersistData;
53 import org.apache.sling.caconfig.spi.ConfigurationPersistData;
54 import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
55 import org.jetbrains.annotations.NotNull;
56 import org.jetbrains.annotations.Nullable;
57 import org.osgi.service.component.annotations.Activate;
58 import org.osgi.service.component.annotations.Component;
59 import org.osgi.service.component.annotations.Reference;
60 import org.osgi.service.metatype.annotations.AttributeDefinition;
61 import org.osgi.service.metatype.annotations.Designate;
62 import org.osgi.service.metatype.annotations.ObjectClassDefinition;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 import com.day.cq.wcm.api.PageManager;
67 import com.day.cq.wcm.api.PageManagerFactory;
68
69 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
70
71
72
73
74
75
76
77 @Component(service = { ConfigurationPersistenceStrategy2.class, ConfigurationResourceResolvingStrategy.class })
78 @Designate(ocd = ToolsConfigPagePersistenceStrategy.Config.class)
79 public class ToolsConfigPagePersistenceStrategy implements ConfigurationPersistenceStrategy2, ConfigurationResourceResolvingStrategy {
80
81 @ObjectClassDefinition(name = "wcm.io Context-Aware Configuration Persistence Strategy: Tools Config Page",
82 description = "Stores Context-Aware Configuration in a single AEM content page at /tools/config.")
83 @interface Config {
84
85 @AttributeDefinition(name = "Enabled",
86 description = "Enable this persistence strategy.")
87 boolean enabled() default false;
88
89 @AttributeDefinition(name = "Config Template",
90 description = "Template that is used for a configuration page.")
91 String configPageTemplate();
92
93 @AttributeDefinition(name = "Structure Template",
94 description = "Template that is used for the tools page.")
95 String structurePageTemplate();
96
97 @AttributeDefinition(name = "Service Ranking",
98 description = "Priority of persistence strategy (higher = higher priority).")
99 int service_ranking() default 2000;
100
101 @AttributeDefinition(name = "Relative config path",
102 description = "Relative path to the configuration page content.")
103 String relativeConfigPath() default "/tools/config/jcr:content";
104
105 @AttributeDefinition(name = "Context path allow list",
106 description = "Expression to match context paths. Context paths matching this expression are allowed.")
107 String contextPathRegex() default "^/content(/.+)$";
108
109 }
110
111 private static final String DEFAULT_CONFIG_NODE_TYPE = NT_UNSTRUCTURED;
112 private static final String PROPERTY_CONFIG_COLLECTION_INHERIT = "sling:configCollectionInherit";
113
114 private static final Logger log = LoggerFactory.getLogger(ToolsConfigPagePersistenceStrategy.class);
115
116 private boolean enabled;
117 private Pattern configPathPattern;
118 private Pattern contextPathPattern;
119 private Config config;
120
121 @Reference
122 private ContextPathStrategyMultiplexer contextPathStrategy;
123 @Reference
124 private ConfigurationManagementSettings configurationManagementSettings;
125 @Reference
126 private PageManagerFactory pageManagerFactory;
127
128
129
130 @Activate
131 void activate(Config value) {
132 this.enabled = value.enabled();
133 this.configPathPattern = loadConfigPathPattern(value);
134 this.contextPathPattern = loadContextPathPattern(value);
135 this.config = value;
136 }
137
138 private @Nullable Pattern loadConfigPathPattern(Config value) {
139 String relativeConfigPath = value.relativeConfigPath();
140 return enabled && StringUtils.isNotBlank(relativeConfigPath)
141 ? Pattern.compile(String.format("^.*%s(/.*)?$", relativeConfigPath))
142 : null;
143 }
144
145 private @Nullable Pattern loadContextPathPattern(Config value) {
146 String contextPathRegex = value.contextPathRegex();
147 return enabled && StringUtils.isNotBlank(contextPathRegex)
148 ? Pattern.compile(contextPathRegex)
149 : null;
150 }
151
152 @Override
153 public Resource getResource(@NotNull Resource resource) {
154 if (!enabled || !isConfigPagePath(resource.getPath())) {
155 return null;
156 }
157 return resource;
158 }
159
160 @Override
161 public Resource getCollectionParentResource(@NotNull Resource resource) {
162 return getResource(resource);
163 }
164
165 @Override
166 public Resource getCollectionItemResource(@NotNull Resource resource) {
167 return getResource(resource);
168 }
169
170 @Override
171 public String getResourcePath(@NotNull String resourcePath) {
172 if (!enabled || !isConfigPagePath(resourcePath)) {
173 return null;
174 }
175 return resourcePath;
176 }
177
178 @Override
179 public String getCollectionParentResourcePath(@NotNull String resourcePath) {
180 return getResourcePath(resourcePath);
181 }
182
183 @Override
184 public String getCollectionItemResourcePath(@NotNull String resourcePath) {
185 return getResourcePath(resourcePath);
186 }
187
188 @Override
189 public String getConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
190 if (!enabled || (relatedConfigPath != null && !isConfigPagePath(relatedConfigPath))) {
191 return null;
192 }
193 return configName;
194 }
195
196 @Override
197 public String getCollectionParentConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
198 return getConfigName(configName, relatedConfigPath);
199 }
200
201 @Override
202 public String getCollectionItemConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
203 return getConfigName(configName, relatedConfigPath);
204 }
205
206 @Override
207 @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
208 public boolean persistConfiguration(@NotNull ResourceResolver resolver, @NotNull String configResourcePath,
209 @NotNull ConfigurationPersistData data) {
210 if (!enabled || !isConfigPagePath(configResourcePath)) {
211 return false;
212 }
213 String path = getResourcePath(configResourcePath);
214 ensureContainingPage(resolver, path, config.configPageTemplate(), null, config.structurePageTemplate(), configurationManagementSettings);
215
216 getOrCreateResource(resolver, path, DEFAULT_CONFIG_NODE_TYPE, data.getProperties(), configurationManagementSettings);
217
218 PageManager pageManager = pageManagerFactory.getPageManager(resolver);
219 updatePageLastMod(resolver, pageManager, path);
220 commit(resolver, configResourcePath);
221 return true;
222 }
223
224 @Override
225 public boolean persistConfigurationCollection(@NotNull ResourceResolver resolver, @NotNull String configResourceCollectionParentPath,
226 @NotNull ConfigurationCollectionPersistData data) {
227 if (!enabled || !isConfigPagePath(configResourceCollectionParentPath)) {
228 return false;
229 }
230 ensureContainingPage(resolver, configResourceCollectionParentPath, config.configPageTemplate(), null, config.structurePageTemplate(),
231 configurationManagementSettings);
232 Resource configResourceParent = getOrCreateResource(resolver, configResourceCollectionParentPath, DEFAULT_CONFIG_NODE_TYPE, ValueMap.EMPTY,
233 configurationManagementSettings);
234
235
236 deleteChildrenNotInCollection(configResourceParent, data);
237 for (ConfigurationPersistData item : data.getItems()) {
238 String path = configResourceParent.getPath() + "/" + item.getCollectionItemName();
239 getOrCreateResource(resolver, path, DEFAULT_CONFIG_NODE_TYPE, item.getProperties(), configurationManagementSettings);
240 }
241
242
243 if (data.getProperties() != null) {
244 replaceProperties(configResourceParent, data.getProperties(), configurationManagementSettings);
245 }
246
247 PageManager pageManager = pageManagerFactory.getPageManager(resolver);
248 updatePageLastMod(resolver, pageManager, configResourceCollectionParentPath);
249 commit(resolver, configResourceCollectionParentPath);
250 return true;
251 }
252
253 @Override
254 public boolean deleteConfiguration(@NotNull ResourceResolver resolver, @NotNull String configResourcePath) {
255 if (!enabled || !isConfigPagePath(configResourcePath)) {
256 return false;
257 }
258 Resource resource = resolver.getResource(configResourcePath);
259 if (resource != null) {
260 deletePageOrResource(resource);
261 }
262 PageManager pageManager = pageManagerFactory.getPageManager(resolver);
263 updatePageLastMod(resolver, pageManager, configResourcePath);
264 commit(resolver, configResourcePath);
265 return true;
266 }
267
268 private boolean isConfigPagePath(String configPath) {
269 return configPathPattern != null && configPathPattern.matcher(configPath).matches();
270 }
271
272
273
274
275
276
277
278 private Iterator<String> findConfigRefs(@NotNull final Resource startResource, @NotNull final Collection<String> bucketNames) {
279
280
281 final Iterator<ContextResource> contextResources = new FilterIterator<>(contextPathStrategy.findContextResources(startResource),
282 contextResource -> StringUtils.isNotBlank(contextResource.getConfigRef()));
283
284
285 final Iterator<String> configPaths = new TransformIterator<>(contextResources,
286 contextResource -> {
287 String val = checkPath(contextResource, contextResource.getConfigRef(), bucketNames);
288 if (val != null) {
289 log.trace("+ Found reference for context path {}: {}", contextResource.getResource().getPath(), val);
290 }
291 return val;
292 });
293 return new FilterIterator<>(configPaths, PredicateUtils.notNullPredicate());
294 }
295
296 private String checkPath(final ContextResource contextResource, final String checkRef, final Collection<String> bucketNames) {
297
298 String ref = ResourceUtil.normalize(checkRef);
299
300 for (String bucketName : bucketNames) {
301 String notAllowedPostfix = "/" + bucketName;
302 if (ref != null && ref.endsWith(notAllowedPostfix)) {
303 log.debug("Ignoring reference to {} from {} - Probably misconfigured as it ends with '{}'",
304 contextResource.getConfigRef(), contextResource.getResource().getPath(), notAllowedPostfix);
305 ref = null;
306 }
307 }
308
309 return ref;
310 }
311
312 @SuppressWarnings("unused")
313 private boolean isEnabledAndParamsValid(final Resource contentResource, final Collection<String> bucketNames, final String configName) {
314 return enabled && contentResource != null && isContextPathAllowed(contentResource.getPath());
315 }
316
317 private boolean isContextPathAllowed(String contextPath) {
318 return contextPathPattern == null || contextPathPattern.matcher(contextPath).matches();
319 }
320
321 private String buildResourcePath(String path, String name) {
322 return ResourceUtil.normalize(path + "/" + name);
323 }
324
325 @Override
326 public Resource getResource(@NotNull final Resource contentResource, @NotNull final Collection<String> bucketNames, @NotNull final String configName) {
327 Iterator<Resource> resources = getResourceInheritanceChain(contentResource, bucketNames, configName);
328 if (resources != null && resources.hasNext()) {
329 return resources.next();
330 }
331 return null;
332 }
333
334 private Iterator<Resource> getResourceInheritanceChainInternal(final Collection<String> bucketNames, final String configName,
335 final Iterator<String> paths, final ResourceResolver resourceResolver) {
336
337
338 Iterator<Resource> matchingResources = IteratorUtils.transformedIterator(paths,
339 path -> {
340 for (String bucketName : bucketNames) {
341 final String name = bucketName + "/" + configName;
342 final String configPath = buildResourcePath(path, name);
343 Resource resource = resourceResolver.getResource(configPath);
344 if (resource != null) {
345 log.trace("+ Found matching config resource for inheritance chain: {}", configPath);
346 return resource;
347 }
348 else {
349 log.trace("- No matching config resource for inheritance chain: {}", configPath);
350 }
351 }
352 return null;
353 });
354 Iterator<Resource> result = IteratorUtils.filteredIterator(matchingResources, PredicateUtils.notNullPredicate());
355 if (result.hasNext()) {
356 return result;
357 }
358 return null;
359 }
360
361 @Override
362 public Iterator<Resource> getResourceInheritanceChain(@NotNull Resource contentResource, @NotNull Collection<String> bucketNames,
363 @NotNull String configName) {
364 if (!isEnabledAndParamsValid(contentResource, bucketNames, configName)) {
365 return null;
366 }
367 final ResourceResolver resourceResolver = contentResource.getResourceResolver();
368
369 Iterator<String> paths = findConfigRefs(contentResource, bucketNames);
370 return getResourceInheritanceChainInternal(bucketNames, configName, paths, resourceResolver);
371 }
372
373 private Collection<Resource> getResourceCollectionInternal(final Collection<String> bucketNames, final String configName,
374 Iterator<String> paths, ResourceResolver resourceResolver) {
375
376 final Map<String, Resource> result = new LinkedHashMap<>();
377
378 boolean inherit = false;
379 while (paths.hasNext()) {
380 final String path = paths.next();
381
382 Resource item = null;
383 for (String bucketName : bucketNames) {
384 String name = bucketName + "/" + configName;
385 String configPath = buildResourcePath(path, name);
386 item = resourceResolver.getResource(configPath);
387 if (item != null) {
388 break;
389 }
390 else {
391 log.trace("- No collection parent resource found: {}", configPath);
392 }
393 }
394
395 if (item != null) {
396 log.trace("o Check children of collection parent resource: {}", item.getPath());
397 if (item.hasChildren()) {
398 for (Resource child : item.getChildren()) {
399 if (isValidResourceCollectionItem(child)
400 && !result.containsKey(child.getName())) {
401 log.trace("+ Found collection resource item {}", child.getPath());
402 result.put(child.getName(), child);
403 }
404 }
405 }
406
407
408 final ValueMap valueMap = item.getValueMap();
409 inherit = valueMap.get(PROPERTY_CONFIG_COLLECTION_INHERIT, false);
410 if (!inherit) {
411 break;
412 }
413 }
414 }
415
416 return result.values();
417 }
418
419 @Override
420 @SuppressWarnings("java:S1168")
421 public Collection<Resource> getResourceCollection(@NotNull final Resource contentResource, @NotNull final Collection<String> bucketNames,
422 @NotNull final String configName) {
423 if (!isEnabledAndParamsValid(contentResource, bucketNames, configName)) {
424 return null;
425 }
426 Iterator<String> paths = findConfigRefs(contentResource, bucketNames);
427 Collection<Resource> result = getResourceCollectionInternal(bucketNames, configName, paths, contentResource.getResourceResolver());
428 if (!result.isEmpty()) {
429 return result;
430 }
431 else {
432 return null;
433 }
434 }
435
436 @Override
437 @SuppressWarnings("java:S1168")
438 public Collection<Iterator<Resource>> getResourceCollectionInheritanceChain(@NotNull final Resource contentResource,
439 @NotNull final Collection<String> bucketNames, @NotNull final String configName) {
440 if (!isEnabledAndParamsValid(contentResource, bucketNames, configName)) {
441 return null;
442 }
443 final ResourceResolver resourceResolver = contentResource.getResourceResolver();
444 final List<String> paths = IteratorUtils.toList(findConfigRefs(contentResource, bucketNames));
445
446
447 Collection<Resource> resourceCollection = getResourceCollectionInternal(bucketNames, configName, paths.iterator(), resourceResolver);
448
449
450
451 Iterator<Iterator<Resource>> result = IteratorUtils.transformedIterator(resourceCollection.iterator(),
452 item -> getResourceInheritanceChainInternal(bucketNames, configName + "/" + item.getName(), paths.iterator(), resourceResolver));
453 if (result.hasNext()) {
454 return IteratorUtils.toList(result);
455 }
456 else {
457 return null;
458 }
459 }
460
461 private boolean isValidResourceCollectionItem(Resource resource) {
462
463 return !StringUtils.equals(resource.getName(), "jcr:content");
464 }
465
466 @Override
467 public String getResourcePath(@NotNull Resource contentResource, @NotNull String bucketName, @NotNull String configName) {
468 if (!isEnabledAndParamsValid(contentResource, Collections.singleton(bucketName), configName)) {
469 return null;
470 }
471 String name = bucketName + "/" + configName;
472
473 Iterator<String> configPaths = this.findConfigRefs(contentResource, Collections.singleton(bucketName));
474 if (configPaths.hasNext()) {
475 String configPath = buildResourcePath(configPaths.next(), name);
476 log.trace("+ Building configuration path for name '{}' for resource {}: {}", name, contentResource.getPath(), configPath);
477 return configPath;
478 }
479 else {
480 log.trace("- No configuration path for name '{}' found for resource {}", name, contentResource.getPath());
481 return null;
482 }
483 }
484
485 @Override
486 public String getResourceCollectionParentPath(@NotNull Resource contentResource, @NotNull String bucketName, @NotNull String configName) {
487 return getResourcePath(contentResource, bucketName, configName);
488 }
489
490 }