View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2017 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
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   * AEM-specific persistence strategy that gets only active if a context path is redirected to path
73   * <code>/content/.../tools/config</code>.
74   * In this case the configuration date is stored in a single page at /tools/config which can be easily activated by
75   * editors via the authoring GUI, and the configuration can neatly be packaged together with the content.
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   // --- ConfigurationPersitenceStrategy ---
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     // delete existing children no longer in the list
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     // if resource collection parent properties are given replace them as well
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   // --- ConfigurationResourceResolvingStrategy ---
274 
275   /**
276    * Searches the resource hierarchy upwards for all config references and returns them.
277    */
278   private Iterator<String> findConfigRefs(@NotNull final Resource startResource, @NotNull final Collection<String> bucketNames) {
279 
280     // collect all context path resources (but filter out those without config reference)
281     final Iterator<ContextResource> contextResources = new FilterIterator<>(contextPathStrategy.findContextResources(startResource),
282         contextResource -> StringUtils.isNotBlank(contextResource.getConfigRef()));
283 
284     // get config resource path for each context resource, filter out items where not reference could be resolved
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     // combine full path if relativeRef is present
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     // find all matching items among all configured paths
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         // check collection inheritance mode on current level - should we check on next-highest level as well?
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     // get resource collection with respect to collection inheritance
447     Collection<Resource> resourceCollection = getResourceCollectionInternal(bucketNames, configName, paths.iterator(), resourceResolver);
448 
449     // get inheritance chain for each item found
450     // yes, this resolves the closest item twice, but is the easiest solution to combine both logic aspects
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     // do not include jcr:content nodes in resource collection list
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 }