| /* |
| * 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.impl; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.commons.collections4.IteratorUtils; |
| import org.apache.commons.collections4.ResettableListIterator; |
| import org.apache.commons.collections4.Transformer; |
| import org.apache.commons.collections4.iterators.ListIteratorWrapper; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceUtil; |
| import org.apache.sling.api.resource.ValueMap; |
| import org.apache.sling.api.wrappers.ValueMapDecorator; |
| import org.apache.sling.caconfig.ConfigurationBuilder; |
| import org.apache.sling.caconfig.ConfigurationResolveException; |
| import org.apache.sling.caconfig.ConfigurationResolver; |
| import org.apache.sling.caconfig.impl.ConfigurationProxy.ChildResolver; |
| import org.apache.sling.caconfig.impl.metadata.AnnotationClassParser; |
| import org.apache.sling.caconfig.management.multiplexer.ConfigurationOverrideMultiplexer; |
| import org.apache.sling.caconfig.management.multiplexer.ConfigurationPersistenceStrategyMultiplexer; |
| import org.apache.sling.caconfig.resource.impl.util.ConfigNameUtil; |
| import org.apache.sling.caconfig.resource.impl.util.MapUtil; |
| import org.apache.sling.caconfig.resource.spi.ConfigurationResourceResolvingStrategy; |
| import org.apache.sling.caconfig.spi.ConfigurationInheritanceStrategy; |
| import org.apache.sling.caconfig.spi.ConfigurationMetadataProvider; |
| import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata; |
| import org.apache.sling.caconfig.spi.metadata.PropertyMetadata; |
| import org.jetbrains.annotations.NotNull; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| class ConfigurationBuilderImpl implements ConfigurationBuilder { |
| |
| private final Resource contentResource; |
| private final ConfigurationResolver configurationResolver; |
| private final ConfigurationResourceResolvingStrategy configurationResourceResolvingStrategy; |
| private final ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy; |
| private final ConfigurationInheritanceStrategy configurationInheritanceStrategy; |
| private final ConfigurationOverrideMultiplexer configurationOverrideMultiplexer; |
| private final ConfigurationMetadataProvider configurationMetadataProvider; |
| private final Collection<String> configBucketNames; |
| private final String configName; |
| |
| private static final Logger log = LoggerFactory.getLogger(ConfigurationBuilderImpl.class); |
| |
| public ConfigurationBuilderImpl(final Resource resource, |
| final ConfigurationResolver configurationResolver, |
| final ConfigurationResourceResolvingStrategy configurationResourceResolvingStrategy, |
| final ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy, |
| final ConfigurationInheritanceStrategy configurationInheritanceStrategy, |
| final ConfigurationOverrideMultiplexer configurationOverrideMultiplexer, |
| final ConfigurationMetadataProvider configurationMetadataProvider, |
| final Collection<String> configBucketNames) { |
| this(resource, configurationResolver, configurationResourceResolvingStrategy, configurationPersistenceStrategy, |
| configurationInheritanceStrategy, configurationOverrideMultiplexer, configurationMetadataProvider, configBucketNames, null); |
| } |
| |
| private ConfigurationBuilderImpl(final Resource resource, |
| final ConfigurationResolver configurationResolver, |
| final ConfigurationResourceResolvingStrategy configurationResourceResolvingStrategy, |
| final ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy, |
| final ConfigurationInheritanceStrategy configurationInheritanceStrategy, |
| final ConfigurationOverrideMultiplexer configurationOverrideMultiplexer, |
| final ConfigurationMetadataProvider configurationMetadataProvider, |
| final Collection<String> configBucketNames, |
| final String configName) { |
| this.contentResource = resource; |
| this.configurationResolver = configurationResolver; |
| this.configurationResourceResolvingStrategy = configurationResourceResolvingStrategy; |
| this.configurationPersistenceStrategy = configurationPersistenceStrategy; |
| this.configurationInheritanceStrategy = configurationInheritanceStrategy; |
| this.configurationOverrideMultiplexer = configurationOverrideMultiplexer; |
| this.configurationMetadataProvider = configurationMetadataProvider; |
| this.configBucketNames = configBucketNames; |
| this.configName = configName; |
| } |
| |
| @Override |
| public @NotNull ConfigurationBuilder name(@NotNull final String configName) { |
| ConfigNameUtil.ensureValidConfigName(configName); |
| return new ConfigurationBuilderImpl(contentResource, |
| configurationResolver, |
| configurationResourceResolvingStrategy, |
| configurationPersistenceStrategy, |
| configurationInheritanceStrategy, |
| configurationOverrideMultiplexer, |
| configurationMetadataProvider, |
| configBucketNames, |
| configName); |
| } |
| |
| /** |
| * Validate the configuration name. |
| * @param name Configuration name or relative path |
| */ |
| private void validateConfigurationName(String name) { |
| if (name == null) { |
| throw new ConfigurationResolveException("Configuration name is required."); |
| } |
| } |
| |
| /** |
| * Converts configuration resource into given class. |
| * @param <T> Target class |
| */ |
| private interface Converter<T> { |
| T convert(Resource resource, Class<T> clazz, String configName, boolean isCollection); |
| } |
| |
| /** |
| * Get singleton configuration resource and convert it to the desired target class. |
| * @param configName Configuration name |
| * @param clazz Target class |
| * @param converter Conversion method |
| * @return Converted singleton configuration |
| */ |
| private <T> T getConfigResource(String configName, Class<T> clazz, Converter<T> converter) { |
| Iterator<Resource> resourceInheritanceChain = null; |
| if (this.contentResource != null) { |
| validateConfigurationName(configName); |
| resourceInheritanceChain = this.configurationResourceResolvingStrategy |
| .getResourceInheritanceChain(this.contentResource, configBucketNames, configName); |
| } |
| return convert(resourceInheritanceChain, clazz, converter, configName, false); |
| } |
| |
| /** |
| * Get configuration resource collection and convert it to the desired target class. |
| * @param configName Configuration name |
| * @param clazz Target class |
| * @param converter Conversion method |
| * @return Converted configuration collection |
| */ |
| private <T> Collection<T> getConfigResourceCollection(String configName, Class<T> clazz, Converter<T> converter) { |
| if (this.contentResource != null) { |
| validateConfigurationName(configName); |
| |
| // get all possible colection parent config names |
| Collection<String> collectionParentConfigNames = configurationPersistenceStrategy.getAllCollectionParentConfigNames(configName); |
| List<Iterator<Resource>> resourceInheritanceChains = new ArrayList<>(); |
| for (String collectionParentConfigName : collectionParentConfigNames) { |
| Collection<Iterator<Resource>> result = this.configurationResourceResolvingStrategy |
| .getResourceCollectionInheritanceChain(this.contentResource, configBucketNames, collectionParentConfigName); |
| if (result != null) { |
| resourceInheritanceChains.addAll(result); |
| } |
| } |
| |
| final Collection<T> result = new ArrayList<>(); |
| for (final Iterator<Resource> resourceInheritanceChain : resourceInheritanceChains) { |
| final T obj = convert(resourceInheritanceChain, clazz, converter, configName, true); |
| if (obj != null) { |
| result.add(obj); |
| } |
| } |
| return result; |
| } |
| else { |
| return Collections.emptyList(); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private <T> T convert(final Iterator<Resource> resourceInhertianceChain, final Class<T> clazz, final Converter<T> converter, |
| final String name, final boolean isCollection) { |
| Resource configResource = null; |
| String conversionName = name; |
| if (resourceInhertianceChain != null) { |
| ResettableListIterator resettableResourceInhertianceChain = new ListIteratorWrapper(resourceInhertianceChain); |
| // apply persistence transformation |
| Iterator<Resource> transformedResources = IteratorUtils.transformedIterator(resettableResourceInhertianceChain, |
| new Transformer() { |
| @Override |
| public Object transform(Object input) { |
| if (isCollection) { |
| return configurationPersistenceStrategy.getCollectionItemResource((Resource)input); |
| } |
| else { |
| return configurationPersistenceStrategy.getResource((Resource)input); |
| } |
| } |
| }); |
| // apply resource inheritance |
| configResource = configurationInheritanceStrategy.getResource(transformedResources); |
| // apply overrides |
| configResource = configurationOverrideMultiplexer.overrideProperties(contentResource.getPath(), name, configResource, contentResource.getResourceResolver()); |
| // build name |
| if (isCollection) { |
| // get untransformed resource for getting collection item name |
| resettableResourceInhertianceChain.reset(); |
| Resource untransformedConfigResource = configurationInheritanceStrategy.getResource(resettableResourceInhertianceChain); |
| if (untransformedConfigResource != null && configResource != null) { |
| conversionName = configurationPersistenceStrategy.getCollectionParentConfigName(conversionName, configResource.getPath()) |
| + "/" + untransformedConfigResource.getName(); |
| } |
| } |
| } |
| if (log.isTraceEnabled() && configResource != null) { |
| log.trace("+ Found config resource for context path " + contentResource.getPath() + ": " + configResource.getPath() + " " |
| + MapUtil.traceOutput(configResource.getValueMap())); |
| } |
| |
| // if no config resource found still check for overrides |
| if (configResource == null && contentResource != null) { |
| configResource = configurationOverrideMultiplexer.overrideProperties(contentResource.getPath(), name, (Resource)null, contentResource.getResourceResolver()); |
| } |
| |
| return converter.convert(configResource, clazz, conversionName, isCollection); |
| } |
| |
| /** |
| * Apply default values from configuration metadata (where no real data is present). |
| * @param resource Resource |
| * @param configName Configuration name |
| * @return null if no default values found, or a wrapped resource with added default properties. |
| */ |
| private Resource applyDefaultValues(Resource resource, String configName) { |
| if (resource == null) { |
| return null; |
| } |
| Map<String,Object> updatedMap = applyDefaultValues(resource.getValueMap(), configName); |
| if (updatedMap == null) { |
| return resource; |
| } |
| return new ConfigurationResourceWrapper(resource, new ValueMapDecorator(updatedMap)); |
| } |
| |
| /** |
| * Apply default values from configuration metadata (where no real data is present). |
| * @param props Properties |
| * @param configName Configuration name |
| * @return null if no default values found, or a new map with added default properties. |
| */ |
| private Map<String,Object> applyDefaultValues(Map<String,Object> props, String configName) { |
| ConfigurationMetadata metadata = configurationMetadataProvider.getConfigurationMetadata(configName); |
| if (metadata == null) { |
| // probably a configuration list - remove item name from end |
| if (StringUtils.contains(configName, "/")) { |
| String partialConfigName = StringUtils.substringBeforeLast(configName, "/"); |
| metadata = configurationMetadataProvider.getConfigurationMetadata(partialConfigName); |
| } |
| if (metadata == null) { |
| return null; |
| } |
| } |
| Map<String,Object> updatedMap = new HashMap<>(); |
| for (PropertyMetadata<?> propertyMetadata : metadata.getPropertyMetadata().values()) { |
| if (propertyMetadata.getDefaultValue() != null) { |
| updatedMap.put(propertyMetadata.getName(), propertyMetadata.getDefaultValue()); |
| } |
| } |
| if (updatedMap.isEmpty()) { |
| return null; |
| } |
| updatedMap.putAll(props); |
| return updatedMap; |
| } |
| |
| // --- Annotation class support --- |
| |
| @Override |
| public @NotNull <T> T as(@NotNull final Class<T> clazz) { |
| final String name = getConfigurationNameForAnnotationClass(clazz); |
| if (log.isDebugEnabled()) { |
| log.debug("Get configuration for context path {}, name '{}', class {}", contentResource.getPath(), name, clazz.getName()); |
| } |
| return getConfigResource(name, clazz, new AnnotationConverter<T>()); |
| } |
| |
| @Override |
| public @NotNull <T> Collection<T> asCollection(@NotNull Class<T> clazz) { |
| final String name = getConfigurationNameForAnnotationClass(clazz); |
| if (log.isDebugEnabled()) { |
| log.debug("Get configuration collection for context path {}, name '{}', class {}", contentResource.getPath(), name, clazz.getName()); |
| } |
| return getConfigResourceCollection(name, clazz, new AnnotationConverter<T>()); |
| } |
| |
| private String getConfigurationNameForAnnotationClass(Class<?> clazz) { |
| if (this.configName != null) { |
| return this.configName; |
| } |
| else { |
| // derive configuration name from annotation class if no name specified |
| return AnnotationClassParser.getConfigurationName(clazz); |
| } |
| } |
| |
| private class AnnotationConverter<T> implements Converter<T> { |
| @Override |
| public T convert(final Resource resource, final Class<T> clazz, final String configName, final boolean isCollection) { |
| return ConfigurationProxy.get(resource, clazz, new ChildResolver() { |
| private ConfigurationBuilder getConfiguration(String nestedConfigName) { |
| String childName; |
| String relatedConfigPath = resource != null ? resource.getPath() : null; |
| if (isCollection) { |
| childName = configurationPersistenceStrategy.getCollectionItemConfigName(configName, relatedConfigPath) + "/" + nestedConfigName; |
| } |
| else { |
| childName = configurationPersistenceStrategy.getConfigName(configName, relatedConfigPath) + "/" + nestedConfigName; |
| } |
| return configurationResolver.get(contentResource).name(childName); |
| } |
| @Override |
| public <C> C getChild(String configName, Class<C> clazz) { |
| return getConfiguration(configName).as(clazz); |
| } |
| @Override |
| public <C> Collection<C> getChildren(String configName, Class<C> clazz) { |
| return getConfiguration(configName).asCollection(clazz); |
| } |
| }); |
| } |
| } |
| |
| // --- ValueMap support --- |
| |
| @Override |
| public @NotNull ValueMap asValueMap() { |
| if (log.isDebugEnabled()) { |
| log.debug("Get ValueMap for context path {}, name '{}'", contentResource.getPath(), this.configName); |
| } |
| return getConfigResource(this.configName, ValueMap.class, new ValueMapConverter()); |
| } |
| |
| @Override |
| public @NotNull Collection<ValueMap> asValueMapCollection() { |
| if (log.isDebugEnabled()) { |
| log.debug("Get ValueMap collection for context path {}, name '{}'", contentResource.getPath(), this.configName); |
| } |
| return getConfigResourceCollection(this.configName, ValueMap.class, new ValueMapConverter()); |
| } |
| |
| private class ValueMapConverter implements Converter<ValueMap> { |
| @Override |
| public ValueMap convert(Resource resource, Class<ValueMap> clazz, String configName, boolean isCollection) { |
| ValueMap props = ResourceUtil.getValueMap(resource); |
| Map<String,Object> updatedMap = applyDefaultValues(props, configName); |
| if (updatedMap != null) { |
| return new ValueMapDecorator(updatedMap); |
| } |
| else { |
| return props; |
| } |
| } |
| } |
| |
| // --- Adaptable support --- |
| |
| @Override |
| public <T> T asAdaptable(@NotNull Class<T> clazz) { |
| if (log.isDebugEnabled()) { |
| log.debug("Get adaptable for context path {}, name '{}', class {}", contentResource.getPath(), this.configName, clazz); |
| } |
| return getConfigResource(this.configName, clazz, new AdaptableConverter<T>()); |
| } |
| |
| @Override |
| public @NotNull <T> Collection<T> asAdaptableCollection(@NotNull Class<T> clazz) { |
| if (log.isDebugEnabled()) { |
| log.debug("Get adaptable collection for context path {}, name '{}', class {}", contentResource.getPath(), this.configName, clazz); |
| } |
| return getConfigResourceCollection(this.configName, clazz, new AdaptableConverter<T>()); |
| } |
| |
| private class AdaptableConverter<T> implements Converter<T> { |
| @Override |
| public T convert(Resource resource, Class<T> clazz, String configName, boolean isCollection) { |
| if (resource == null || clazz == ConfigurationBuilder.class) { |
| return null; |
| } |
| return applyDefaultValues(resource, configName).adaptTo(clazz); |
| } |
| } |
| |
| // --- Config Node Existence Check Support --- |
| |
| @Override |
| public <T> boolean has(@NotNull Class<T> clazz) { |
| final String name = getConfigurationNameForAnnotationClass(clazz); |
| if (log.isDebugEnabled()) { |
| log.debug("Check configuration for context path {}, name '{}', class {}", contentResource.getPath(), name, clazz.getName()); |
| } |
| return checkIfConfigNodeExists(name); |
| } |
| |
| @Override |
| public boolean has(@NotNull String configName) { |
| if (log.isDebugEnabled()) { |
| log.debug("Check configuration for context path {}, configuration name '{}' ", contentResource.getPath(), configName); |
| } |
| return checkIfConfigNodeExists(configName); |
| } |
| |
| private <T> boolean checkIfConfigNodeExists(String configName) { |
| Resource configResource = null; |
| if (this.contentResource != null) { |
| validateConfigurationName(configName); |
| configResource = this.configurationResourceResolvingStrategy |
| .getResource(this.contentResource, configBucketNames, configName); |
| } |
| return configResource != null ? true : false; |
| } |
| |
| } |