| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| package org.apache.sling.caconfig.resource.impl.def; |
| |
| import static org.apache.sling.caconfig.resource.impl.def.ConfigurationResourceNameConstants.PROPERTY_CONFIG_COLLECTION_INHERIT; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| import org.apache.commons.collections.IteratorUtils; |
| import org.apache.commons.collections.Predicate; |
| import org.apache.commons.collections.PredicateUtils; |
| import org.apache.commons.collections.Transformer; |
| import org.apache.commons.collections.iterators.ArrayIterator; |
| import org.apache.commons.collections.iterators.FilterIterator; |
| import org.apache.commons.collections.iterators.IteratorChain; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceResolver; |
| import org.apache.sling.api.resource.ResourceUtil; |
| import org.apache.sling.api.resource.ValueMap; |
| import org.apache.sling.caconfig.management.multiplexer.ContextPathStrategyMultiplexer; |
| import org.apache.sling.caconfig.resource.impl.util.ConfigNameUtil; |
| import org.apache.sling.caconfig.resource.impl.util.PathEliminateDuplicatesIterator; |
| import org.apache.sling.caconfig.resource.impl.util.PathParentExpandIterator; |
| import org.apache.sling.caconfig.resource.impl.util.PropertyUtil; |
| import org.apache.sling.caconfig.resource.spi.CollectionInheritanceDecider; |
| import org.apache.sling.caconfig.resource.spi.ConfigurationResourceResolvingStrategy; |
| import org.apache.sling.caconfig.resource.spi.ContextResource; |
| import org.apache.sling.caconfig.resource.spi.InheritanceDecision; |
| import org.apache.sling.settings.SlingSettingsService; |
| import org.osgi.service.component.annotations.Activate; |
| import org.osgi.service.component.annotations.Component; |
| import org.osgi.service.component.annotations.Deactivate; |
| import org.osgi.service.component.annotations.FieldOption; |
| import org.osgi.service.component.annotations.Reference; |
| import org.osgi.service.component.annotations.ReferenceCardinality; |
| import org.osgi.service.component.annotations.ReferencePolicy; |
| import org.osgi.service.metatype.annotations.AttributeDefinition; |
| import org.osgi.service.metatype.annotations.Designate; |
| import org.osgi.service.metatype.annotations.ObjectClassDefinition; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| @Component(service=ConfigurationResourceResolvingStrategy.class) |
| @Designate(ocd=DefaultConfigurationResourceResolvingStrategy.Config.class) |
| public class DefaultConfigurationResourceResolvingStrategy implements ConfigurationResourceResolvingStrategy { |
| |
| @ObjectClassDefinition(name="Apache Sling Context-Aware Configuration Default Resource Resolving Strategy", |
| description="Standardized access to configurations in the resource tree.") |
| public static @interface Config { |
| |
| @AttributeDefinition(name="Enabled", |
| description = "Enable this configuration resource resolving strategy.") |
| boolean enabled() default true; |
| |
| @AttributeDefinition(name="Configurations path", |
| description = "Paths where the configurations are stored in.") |
| String configPath() default "/conf"; |
| |
| @AttributeDefinition(name="Fallback paths", |
| description = "Global fallback configurations, ordered from most specific (checked first) to least specific.") |
| String[] fallbackPaths() default {"/conf/global", "/apps/conf", "/libs/conf"}; |
| |
| @AttributeDefinition(name="Config collection inheritance property names", |
| description = "Additional property names to " + PROPERTY_CONFIG_COLLECTION_INHERIT + " to handle configuration inheritance. The names are used in the order defined, " |
| + "always starting with " + PROPERTY_CONFIG_COLLECTION_INHERIT + ". Once a property with a value is found, that value is used and the following property names are skipped.") |
| String[] configCollectionInheritancePropertyNames(); |
| |
| @AttributeDefinition(name = "Run-mode aware", |
| description = "Also resolve configuration stored in run-mode specific buckets named '<bucketname>.<runmode>'.") |
| boolean runModeAware() default false; |
| |
| } |
| |
| private static final Logger log = LoggerFactory.getLogger(DefaultConfigurationResourceResolvingStrategy.class); |
| |
| private volatile Config config; |
| |
| @Reference |
| private ContextPathStrategyMultiplexer contextPathStrategy; |
| |
| @Reference(cardinality=ReferenceCardinality.MULTIPLE, |
| policy=ReferencePolicy.DYNAMIC, |
| fieldOption=FieldOption.REPLACE) |
| private volatile List<CollectionInheritanceDecider> collectionInheritanceDeciders; |
| |
| @Reference |
| private SlingSettingsService slingSettings; |
| |
| @Activate |
| private void activate(final Config config) { |
| this.config = config; |
| } |
| |
| @Deactivate |
| private void deactivate() { |
| this.config = null; |
| } |
| |
| @SuppressWarnings("unchecked") |
| Iterator<String> getResolvePaths(final Resource contentResource, final Collection<String> bucketNames) { |
| return new IteratorChain( |
| // add all config references found in resource hierarchy |
| findConfigRefs(contentResource, bucketNames), |
| // finally add the global fallbacks |
| new ArrayIterator(this.config.fallbackPaths()) |
| ); |
| } |
| |
| /** |
| * Searches the resource hierarchy upwards for all config references and returns them. |
| * @param startResource Resource to start searching |
| * @param bucketNames Bucket names to search in |
| */ |
| @SuppressWarnings("unchecked") |
| private Iterator<String> findConfigRefs(final Resource startResource, final Collection<String> bucketNames) { |
| |
| // collect all context path resources (but filter out those without config reference) |
| final Iterator<ContextResource> contextResources = new FilterIterator(contextPathStrategy.findContextResources(startResource), |
| new Predicate() { |
| @Override |
| public boolean evaluate(Object object) { |
| ContextResource contextResource = (ContextResource)object; |
| return StringUtils.isNotBlank(contextResource.getConfigRef()); |
| } |
| }); |
| |
| // get config resource path for each context resource, filter out items where not reference could be resolved |
| final Iterator<String> configPaths = new Iterator<String>() { |
| |
| private final List<ContextResource> relativePaths = new ArrayList<>(); |
| |
| private String next = seek(); |
| |
| private String useFromRelativePathsWith; |
| |
| private String seek() { |
| String val = null; |
| while ( val == null && (useFromRelativePathsWith != null || contextResources.hasNext()) ) { |
| if ( useFromRelativePathsWith != null ) { |
| final ContextResource contextResource = relativePaths.remove(relativePaths.size() - 1); |
| val = checkPath(contextResource, useFromRelativePathsWith + "/" + contextResource.getConfigRef(), bucketNames); |
| if (val != null) { |
| log.trace("+ Found reference for context path {}: {}", contextResource.getResource().getPath(), val); |
| } |
| if ( relativePaths.isEmpty() ) { |
| useFromRelativePathsWith = null; |
| } |
| } else { |
| final ContextResource contextResource = contextResources.next(); |
| val = contextResource.getConfigRef(); |
| |
| // if absolute path found we are (probably) done |
| if (val != null && val.startsWith("/")) { |
| val = checkPath(contextResource, val, bucketNames); |
| } |
| |
| if (val != null) { |
| final boolean isAbsolute = val.startsWith("/"); |
| if ( isAbsolute && !relativePaths.isEmpty() ) { |
| useFromRelativePathsWith = val; |
| val = null; |
| } else if ( !isAbsolute ) { |
| relativePaths.add(0, contextResource); |
| val = null; |
| } |
| } |
| |
| if (val != null) { |
| log.trace("+ Found reference for context path {}: {}", contextResource.getResource().getPath(), val); |
| } |
| } |
| } |
| if ( val == null && !relativePaths.isEmpty() ) { |
| log.error("Relative references not used as no absolute reference was found: {}", relativePaths); |
| } |
| return val; |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return next != null; |
| } |
| |
| @Override |
| public String next() { |
| if ( next == null ) { |
| throw new NoSuchElementException(); |
| } |
| final String result = next; |
| next = seek(); |
| return result; |
| } |
| |
| @Override |
| public void remove() { |
| throw new UnsupportedOperationException(); |
| } |
| }; |
| |
| // expand paths and eliminate duplicates |
| return new PathEliminateDuplicatesIterator(new PathParentExpandIterator(config.configPath(), configPaths)); |
| } |
| |
| private String checkPath(final ContextResource contextResource, String ref, final Collection<String> bucketNames) { |
| // combine full path if relativeRef is present |
| ref = ResourceUtil.normalize(ref); |
| |
| for (String bucketName : bucketNames) { |
| String notAllowedPostfix = "/" + bucketName; |
| if (ref != null && ref.endsWith(notAllowedPostfix)) { |
| log.warn("Ignoring reference to {} from {} - Probably misconfigured as it ends with '{}'", |
| contextResource.getConfigRef(), contextResource.getResource().getPath(), notAllowedPostfix); |
| ref = null; |
| } |
| } |
| |
| if (ref != null && !isAllowedConfigPath(ref)) { |
| log.debug("Ignoring reference to {} from {} - not in allowed paths.", |
| contextResource.getConfigRef(), contextResource.getResource().getPath()); |
| ref = null; |
| } |
| |
| if (ref != null && isFallbackConfigPath(ref)) { |
| log.debug("Ignoring reference to {} from {} - already a fallback path.", |
| contextResource.getConfigRef(), contextResource.getResource().getPath()); |
| ref = null; |
| } |
| |
| return ref; |
| } |
| |
| private boolean isAllowedConfigPath(String path) { |
| return path.startsWith(config.configPath() + "/"); |
| } |
| |
| private boolean isFallbackConfigPath(final String ref) { |
| for(final String path : this.config.fallbackPaths()) { |
| if (StringUtils.equals(ref, path) || StringUtils.startsWith(ref, path + "/")) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isEnabledAndParamsValid(final Resource contentResource, final Collection<String> bucketNames, final String configName) { |
| return config.enabled() && contentResource != null && ConfigNameUtil.isValid(bucketNames) && ConfigNameUtil.isValid(configName); |
| } |
| |
| private String buildResourcePath(String path, String name) { |
| return ResourceUtil.normalize(path + "/" + name); |
| } |
| |
| @Override |
| public Resource getResource(final Resource contentResource, final Collection<String> bucketNames, final String configName) { |
| Iterator<Resource> resources = getResourceInheritanceChain(contentResource, bucketNames, configName); |
| if (resources != null && resources.hasNext()) { |
| return resources.next(); |
| } |
| return null; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Iterator<Resource> getResourceInheritanceChainInternal(final Collection<String> bucketNames, final String configName, |
| final Iterator<String> paths, final ResourceResolver resourceResolver) { |
| |
| // find all matching items among all configured paths |
| Iterator<Resource> matchingResources = IteratorUtils.transformedIterator(paths, new Transformer() { |
| @Override |
| public Object transform(Object input) { |
| String path = (String)input; |
| for (String bucketName : bucketNames) { |
| final String name = bucketName + "/" + configName; |
| final String configPath = buildResourcePath(path, name); |
| Resource resource = resourceResolver.getResource(configPath); |
| if (resource != null) { |
| log.trace("+ Found matching config resource for inheritance chain: {}", configPath); |
| return resource; |
| } |
| else { |
| log.trace("- No matching config resource for inheritance chain: {}", configPath); |
| } |
| } |
| return null; |
| } |
| }); |
| Iterator<Resource> result = IteratorUtils.filteredIterator(matchingResources, PredicateUtils.notNullPredicate()); |
| if (result.hasNext()) { |
| return result; |
| } |
| return null; |
| } |
| |
| @Override |
| public Iterator<Resource> getResourceInheritanceChain(Resource contentResource, Collection<String> bucketNames, String configName) { |
| final Collection<String> expandedBucketNames = expandBucketNames(bucketNames); |
| if (!isEnabledAndParamsValid(contentResource, expandedBucketNames, configName)) { |
| return null; |
| } |
| final ResourceResolver resourceResolver = contentResource.getResourceResolver(); |
| |
| Iterator<String> paths = getResolvePaths(contentResource, expandedBucketNames); |
| return getResourceInheritanceChainInternal(expandedBucketNames, configName, paths, resourceResolver); |
| } |
| |
| private boolean include(final List<CollectionInheritanceDecider> deciders, |
| final String bucketName, |
| final Resource resource, |
| final Set<String> blockedItems) { |
| boolean result = !blockedItems.contains(resource.getName()); |
| if ( result && deciders != null && !deciders.isEmpty() ) { |
| for(int i=deciders.size()-1;i>=0;i--) { |
| final InheritanceDecision decision = deciders.get(i).decide(resource, bucketName); |
| if ( decision == InheritanceDecision.EXCLUDE ) { |
| log.trace("- Block resource collection inheritance for bucket {}, resource {} because {} retruned EXCLUDE.", |
| bucketName, resource.getPath(), deciders.get(i)); |
| result = false; |
| break; |
| } else if ( decision == InheritanceDecision.BLOCK ) { |
| log.trace("- Block resource collection inheritance for bucket {}, resource {} because {} retruned BLOCK.", |
| bucketName, resource.getPath(), deciders.get(i)); |
| result = false; |
| blockedItems.add(resource.getName()); |
| break; |
| } |
| } |
| } |
| return result; |
| } |
| |
| private Collection<Resource> getResourceCollectionInternal(final Collection<String> bucketNames, final String configName, |
| Iterator<String> paths, ResourceResolver resourceResolver) { |
| |
| final Map<String,Resource> result = new LinkedHashMap<>(); |
| final List<CollectionInheritanceDecider> deciders = this.collectionInheritanceDeciders; |
| final Set<String> blockedItems = new HashSet<>(); |
| |
| while (paths.hasNext()) { |
| final String path = paths.next(); |
| |
| boolean inherit = false; |
| boolean foundAnyParent = false; |
| for (String bucketName : bucketNames) { |
| String name = bucketName + "/" + configName; |
| String configPath = buildResourcePath(path, name); |
| Resource item = resourceResolver.getResource(configPath); |
| if (item != null) { |
| log.trace("o Check children of collection parent resource: {}", item.getPath()); |
| if (item.hasChildren()) { |
| for (Resource child : item.getChildren()) { |
| if (isValidResourceCollectionItem(child) |
| && !result.containsKey(child.getName()) |
| && include(deciders, bucketName, child, blockedItems)) { |
| log.trace("+ Found collection resource item {}", child.getPath()); |
| result.put(child.getName(), child); |
| } |
| } |
| } |
| |
| // check collection inheritance mode on current level - should we check on next-highest level as well? |
| final ValueMap valueMap = item.getValueMap(); |
| inherit = inherit || PropertyUtil.getBooleanValueAdditionalKeys(valueMap, PROPERTY_CONFIG_COLLECTION_INHERIT, |
| config.configCollectionInheritancePropertyNames()); |
| foundAnyParent = true; |
| } |
| else { |
| log.trace("- No collection parent resource found: {}", configPath); |
| } |
| } |
| if (foundAnyParent && !inherit) { |
| break; |
| } |
| } |
| |
| return result.values(); |
| } |
| |
| @Override |
| public Collection<Resource> getResourceCollection(final Resource contentResource, final Collection<String> bucketNames, final String configName) { |
| final Collection<String> expandedBucketNames = expandBucketNames(bucketNames); |
| if (!isEnabledAndParamsValid(contentResource, expandedBucketNames, configName)) { |
| return null; |
| } |
| Iterator<String> paths = getResolvePaths(contentResource, expandedBucketNames); |
| Collection<Resource> result = getResourceCollectionInternal(expandedBucketNames, configName, paths, contentResource.getResourceResolver()); |
| if (!result.isEmpty()) { |
| return result; |
| } |
| else { |
| return null; |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public Collection<Iterator<Resource>> getResourceCollectionInheritanceChain(final Resource contentResource, |
| final Collection<String> bucketNames, final String configName) { |
| final Collection<String> expandedBucketNames = expandBucketNames(bucketNames); |
| if (!isEnabledAndParamsValid(contentResource, expandedBucketNames, configName)) { |
| return null; |
| } |
| final ResourceResolver resourceResolver = contentResource.getResourceResolver(); |
| final List<String> paths = IteratorUtils.toList(getResolvePaths(contentResource, expandedBucketNames)); |
| |
| // get resource collection with respect to collection inheritance |
| Collection<Resource> resourceCollection = getResourceCollectionInternal(expandedBucketNames, configName, paths.iterator(), resourceResolver); |
| |
| // get inheritance chain for each item found |
| // yes, this resolves the closest item twice, but is the easiest solution to combine both logic aspects |
| Iterator<Iterator<Resource>> result = IteratorUtils.transformedIterator(resourceCollection.iterator(), new Transformer() { |
| @Override |
| public Object transform(Object input) { |
| Resource item = (Resource)input; |
| return getResourceInheritanceChainInternal(expandedBucketNames, configName + "/" + item.getName(), paths.iterator(), resourceResolver); |
| } |
| }); |
| if (result.hasNext()) { |
| return IteratorUtils.toList(result); |
| } |
| else { |
| return null; |
| } |
| } |
| |
| private boolean isValidResourceCollectionItem(Resource resource) { |
| // do not include jcr:content nodes in resource collection list |
| return !StringUtils.equals(resource.getName(), "jcr:content"); |
| } |
| |
| @Override |
| public String getResourcePath(Resource contentResource, String bucketName, String configName) { |
| if (!isEnabledAndParamsValid(contentResource, Collections.singleton(bucketName), configName)) { |
| return null; |
| } |
| String name = bucketName + "/" + configName; |
| |
| Iterator<String> configPaths = this.findConfigRefs(contentResource, Collections.singleton(bucketName)); |
| if (configPaths.hasNext()) { |
| String configPath = buildResourcePath(configPaths.next(), name); |
| log.trace("+ Building configuration path for name '{}' for resource {}: {}", name, contentResource.getPath(), configPath); |
| return configPath; |
| } |
| else { |
| log.trace("- No configuration path for name '{}' found for resource {}", name, contentResource.getPath()); |
| return null; |
| } |
| } |
| |
| @Override |
| public String getResourceCollectionParentPath(Resource contentResource, String bucketName, String configName) { |
| return getResourcePath(contentResource, bucketName, configName); |
| } |
| |
| /** |
| * Expands list of bucket names if the runmode-aware feature is activated. |
| * Adds a variant for each given bucket name with a suffix ".{rundmode}" attached. |
| * The list returned contains the entries with runmode suffix first so they have higher precedence than the original buckets. |
| * @param bucketNames Original bucket names |
| * @return Expanded bucket names |
| */ |
| private Collection<String> expandBucketNames(Collection<String> bucketNames) { |
| if (!config.runModeAware()) { |
| return bucketNames; |
| } |
| |
| Set<String> runModes = slingSettings.getRunModes(); |
| List<String> expandedBucketNames = new ArrayList<>((runModes.size() + 1) * bucketNames.size()); |
| for (String originalBucketName : bucketNames) { |
| for (String runMode : runModes) { |
| expandedBucketNames.add(originalBucketName + "." + runMode); |
| } |
| expandedBucketNames.add(originalBucketName); |
| } |
| if (log.isTraceEnabled()) { |
| log.trace("o Use expanded list of bucket names: " + expandedBucketNames); |
| } |
| return expandedBucketNames; |
| } |
| |
| } |