blob: 307de4e2380c92908309d4819bd2adf653b8cf2d [file] [log] [blame]
/*
* 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;
}
}