package org.apereo.cas.configuration.config;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apereo.cas.configuration.CasConfigurationPropertiesEnvironmentManager;
import org.apereo.cas.configuration.support.CasConfigurationJasyptDecryptor;
import org.jooq.lambda.Unchecked;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.YamlProcessor;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
/**
* This is {@link CasCoreBootstrapStandaloneConfiguration}.
*
* @author Misagh Moayyed
* @since 5.1.0
*/
@Profile("standalone")
@ConditionalOnProperty(value = "spring.cloud.config.enabled", havingValue = "false")
@Configuration("casStandaloneBootstrapConfiguration")
public class CasCoreBootstrapStandaloneConfiguration implements PropertySourceLocator {
private static final Logger LOGGER = LoggerFactory.getLogger(CasCoreBootstrapStandaloneConfiguration.class);
private CasConfigurationJasyptDecryptor configurationJasyptDecryptor;
@Autowired
private ResourceLoader resourceLoader;
@ConfigurationPropertiesBinding
@Bean
public Converter<String, List<Class<? extends Throwable>>> commaSeparatedStringToThrowablesCollection() {
return new Converter<String, List<Class<? extends Throwable>>>() {
@Override
public List<Class<? extends Throwable>> convert(final String source) {
try {
final List<Class<? extends Throwable>> classes = new ArrayList<>();
for (final String className : StringUtils.commaDelimitedListToStringArray(source)) {
classes.add((Class<? extends Throwable>) ClassUtils.forName(className.trim(), getClass().getClassLoader()));
}
return classes;
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
};
}
@Bean
public CasConfigurationPropertiesEnvironmentManager configurationPropertiesEnvironmentManager() {
return new CasConfigurationPropertiesEnvironmentManager();
}
@Override
public PropertySource<?> locate(final Environment environment) {
this.configurationJasyptDecryptor = new CasConfigurationJasyptDecryptor(environment);
final Properties props = new Properties();
loadEmbeddedYamlOverriddenProperties(props, environment);
final File configFile = configurationPropertiesEnvironmentManager().getStandaloneProfileConfigurationFile();
if (configFile != null) {
loadSettingsFromStandaloneConfigFile(props, configFile);
}
final File config = configurationPropertiesEnvironmentManager().getStandaloneProfileConfigurationDirectory();
LOGGER.debug("Located CAS standalone configuration directory at [{}]", config);
if (config.isDirectory() && config.exists()) {
loadSettingsFromConfigurationSources(environment, props, config);
} else {
LOGGER.warn("Configuration directory [{}] is not a directory or cannot be found at the specific path", config);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Located setting(s) [{}] from [{}]", props.keySet(), config);
} else {
LOGGER.info("Found and loaded [{}] setting(s) from [{}]", props.size(), config);
}
return new PropertiesPropertySource("standaloneCasConfigService", props);
}
private void loadSettingsFromStandaloneConfigFile(final Properties props, final File configFile) {
try {
LOGGER.debug("Located CAS standalone configuration file at [{}]", configFile);
final Properties pp = new Properties();
pp.load(new FileReader(configFile));
LOGGER.debug("Found settings [{}] in file [{}]", pp.keySet(), configFile);
props.putAll(decryptProperties(pp));
} catch (final Exception e) {
LOGGER.warn(e.getMessage(), e);
}
}
private Map<Object, Object> decryptProperties(final Map<Object, Object> properties) {
return this.configurationJasyptDecryptor.decrypt(properties);
}
private void loadSettingsFromConfigurationSources(final Environment environment, final Properties props, final File config) {
final List<String> profiles = getApplicationProfiles(environment);
final String regex = buildPatternForConfigurationFileDiscovery(config, profiles);
final Collection<File> configFiles = scanForConfigurationFilesByPattern(config, regex);
LOGGER.info("Configuration files found at [{}] are [{}]", config, configFiles);
configFiles.forEach(Unchecked.consumer(f -> {
LOGGER.debug("Loading configuration file [{}]", f);
if (f.getName().toLowerCase().endsWith("yml")) {
final Map<Object, Object> pp = loadYamlProperties(new FileSystemResource(f));
LOGGER.debug("Found settings [{}] in YAML file [{}]", pp.keySet(), f);
props.putAll(decryptProperties(pp));
} else {
final Properties pp = new Properties();
pp.load(new FileReader(f));
LOGGER.debug("Found settings [{}] in file [{}]", pp.keySet(), f);
props.putAll(decryptProperties(pp));
}
}));
}
private static Collection<File> scanForConfigurationFilesByPattern(final File config, final String regex) {
return FileUtils.listFiles(config, new RegexFileFilter(regex, IOCase.INSENSITIVE), TrueFileFilter.INSTANCE)
.stream()
.sorted(Comparator.comparing(File::getName))
.collect(Collectors.toList());
}
private static String buildPatternForConfigurationFileDiscovery(final File config, final List<String> profiles) {
final String propertyNames = profiles.stream().collect(Collectors.joining("|"));
final String profiledProperties = profiles.stream()
.map(p -> String.format("application-%s", p))
.collect(Collectors.joining("|"));
final String regex = String.format("(%s|%s|application)\\.(yml|properties)", propertyNames, profiledProperties);
LOGGER.debug("Looking for configuration files at [{}] that match the pattern [{}]", config, regex);
return regex;
}
private List<String> getApplicationProfiles(final Environment environment) {
final List<String> profiles = new ArrayList<>();
profiles.add(configurationPropertiesEnvironmentManager().getApplicationName());
profiles.addAll(Arrays.stream(environment.getActiveProfiles()).collect(Collectors.toList()));
return profiles;
}
private static Map<Object, Object> loadYamlProperties(final Resource... resource) {
final YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResolutionMethod(YamlProcessor.ResolutionMethod.OVERRIDE);
factory.setResources(resource);
factory.setSingleton(true);
factory.afterPropertiesSet();
return factory.getObject();
}
private void loadEmbeddedYamlOverriddenProperties(final Properties props, final Environment environment) {
final Resource resource = resourceLoader.getResource("classpath:/application.yml");
if (resource != null && resource.exists()) {
final Map pp = loadYamlProperties(resource);
if (pp.isEmpty()) {
LOGGER.debug("No properties were located inside [{}]", resource);
} else {
LOGGER.debug("Found settings [{}] in YAML file [{}]", pp.keySet(), resource);
props.putAll(decryptProperties(pp));
}
}
}
}