Introduction
Dhall is a programmable configuration language that provides safety, flexibility, and powerful features for configuration management. This guide covers how to integrate Dhall configuration files into Java applications.
Dependencies and Setup
1. Maven Dependencies
<properties>
<dhall.java.version>1.0.0</dhall.java.version>
<jackson.version>2.15.3</jackson.version>
<spring-boot.version>3.2.0</spring-boot.version>
</properties>
<dependencies>
<!-- Dhall Java Integration -->
<dependency>
<groupId>org.dhall</groupId>
<artifactId>dhall-java</artifactId>
<version>${dhall.java.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Core Dhall Models
2. Configuration Data Classes
package com.example.dhall.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.Duration;
import java.util.List;
import java.util.Map;
public class ApplicationConfig {
@NotBlank
private String name;
@NotBlank
private String version;
@NotNull
private ServerConfig server;
@NotNull
private DatabaseConfig database;
private CacheConfig cache;
private SecurityConfig security;
private List<FeatureFlag> featureFlags;
private Map<String, Object> customSettings;
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public ServerConfig getServer() { return server; }
public void setServer(ServerConfig server) { this.server = server; }
public DatabaseConfig getDatabase() { return database; }
public void setDatabase(DatabaseConfig database) { this.database = database; }
public CacheConfig getCache() { return cache; }
public void setCache(CacheConfig cache) { this.cache = cache; }
public SecurityConfig getSecurity() { return security; }
public void setSecurity(SecurityConfig security) { this.security = security; }
public List<FeatureFlag> getFeatureFlags() { return featureFlags; }
public void setFeatureFlags(List<FeatureFlag> featureFlags) { this.featureFlags = featureFlags; }
public Map<String, Object> getCustomSettings() { return customSettings; }
public void setCustomSettings(Map<String, Object> customSettings) { this.customSettings = customSettings; }
public static class ServerConfig {
@Min(1)
@Max(65535)
private int port = 8080;
private String host = "localhost";
@NotNull
private Duration timeout = Duration.ofSeconds(30);
private int maxThreads = 50;
private boolean sslEnabled = false;
private SslConfig ssl;
// Getters and Setters
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public Duration getTimeout() { return timeout; }
public void setTimeout(Duration timeout) { this.timeout = timeout; }
public int getMaxThreads() { return maxThreads; }
public void setMaxThreads(int maxThreads) { this.maxThreads = maxThreads; }
public boolean isSslEnabled() { return sslEnabled; }
public void setSslEnabled(boolean sslEnabled) { this.sslEnabled = sslEnabled; }
public SslConfig getSsl() { return ssl; }
public void setSsl(SslConfig ssl) { this.ssl = ssl; }
public static class SslConfig {
@NotBlank
private String keyStore;
@NotBlank
private String keyStorePassword;
private String keyAlias;
private List<String> protocols = List.of("TLSv1.2", "TLSv1.3");
// Getters and Setters
public String getKeyStore() { return keyStore; }
public void setKeyStore(String keyStore) { this.keyStore = keyStore; }
public String getKeyStorePassword() { return keyStorePassword; }
public void setKeyStorePassword(String keyStorePassword) { this.keyStorePassword = keyStorePassword; }
public String getKeyAlias() { return keyAlias; }
public void setKeyAlias(String keyAlias) { this.keyAlias = keyAlias; }
public List<String> getProtocols() { return protocols; }
public void setProtocols(List<String> protocols) { this.protocols = protocols; }
}
}
public static class DatabaseConfig {
@NotBlank
private String url;
@NotBlank
private String username;
private String password;
private String driverClassName = "org.postgresql.Driver";
private PoolConfig pool;
private Map<String, String> properties;
// Getters and Setters
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getDriverClassName() { return driverClassName; }
public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; }
public PoolConfig getPool() { return pool; }
public void setPool(PoolConfig pool) { this.pool = pool; }
public Map<String, String> getProperties() { return properties; }
public void setProperties(Map<String, String> properties) { this.properties = properties; }
public static class PoolConfig {
@Min(1)
private int minSize = 5;
@Min(1)
private int maxSize = 20;
@NotNull
private Duration connectionTimeout = Duration.ofSeconds(30);
@NotNull
private Duration idleTimeout = Duration.ofMinutes(10);
@NotNull
private Duration maxLifetime = Duration.ofMinutes(30);
// Getters and Setters
public int getMinSize() { return minSize; }
public void setMinSize(int minSize) { this.minSize = minSize; }
public int getMaxSize() { return maxSize; }
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
public Duration getConnectionTimeout() { return connectionTimeout; }
public void setConnectionTimeout(Duration connectionTimeout) { this.connectionTimeout = connectionTimeout; }
public Duration getIdleTimeout() { return idleTimeout; }
public void setIdleTimeout(Duration idleTimeout) { this.idleTimeout = idleTimeout; }
public Duration getMaxLifetime() { return maxLifetime; }
public void setMaxLifetime(Duration maxLifetime) { this.maxLifetime = maxLifetime; }
}
}
public static class CacheConfig {
private boolean enabled = true;
@NotNull
private Duration defaultTtl = Duration.ofMinutes(30);
private RedisConfig redis;
private LocalCacheConfig local;
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public Duration getDefaultTtl() { return defaultTtl; }
public void setDefaultTtl(Duration defaultTtl) { this.defaultTtl = defaultTtl; }
public RedisConfig getRedis() { return redis; }
public void setRedis(RedisConfig redis) { this.redis = redis; }
public LocalCacheConfig getLocal() { return local; }
public void setLocal(LocalCacheConfig local) { this.local = local; }
public static class RedisConfig {
@NotBlank
private String host = "localhost";
@Min(1)
@Max(65535)
private int port = 6379;
private String password;
private int database = 0;
@NotNull
private Duration timeout = Duration.ofSeconds(10);
// Getters and Setters
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public int getDatabase() { return database; }
public void setDatabase(int database) { this.database = database; }
public Duration getTimeout() { return timeout; }
public void setTimeout(Duration timeout) { this.timeout = timeout; }
}
public static class LocalCacheConfig {
@Min(1)
private int maxSize = 1000;
@NotNull
private Duration ttl = Duration.ofMinutes(10);
// Getters and Setters
public int getMaxSize() { return maxSize; }
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
public Duration getTtl() { return ttl; }
public void setTtl(Duration ttl) { this.ttl = ttl; }
}
}
public static class SecurityConfig {
private boolean enabled = true;
private JwtConfig jwt;
private CorsConfig cors;
private List<String> allowedOrigins = List.of("*");
private RateLimitConfig rateLimit;
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public JwtConfig getJwt() { return jwt; }
public void setJwt(JwtConfig jwt) { this.jwt = jwt; }
public CorsConfig getCors() { return cors; }
public void setCors(CorsConfig cors) { this.cors = cors; }
public List<String> getAllowedOrigins() { return allowedOrigins; }
public void setAllowedOrigins(List<String> allowedOrigins) { this.allowedOrigins = allowedOrigins; }
public RateLimitConfig getRateLimit() { return rateLimit; }
public void setRateLimit(RateLimitConfig rateLimit) { this.rateLimit = rateLimit; }
public static class JwtConfig {
@NotBlank
private String secret;
@NotNull
private Duration expiration = Duration.ofHours(24);
private String issuer = "myapp";
private List<String> audiences = List.of("web", "mobile");
// Getters and Setters
public String getSecret() { return secret; }
public void setSecret(String secret) { this.secret = secret; }
public Duration getExpiration() { return expiration; }
public void setExpiration(Duration expiration) { this.expiration = expiration; }
public String getIssuer() { return issuer; }
public void setIssuer(String issuer) { this.issuer = issuer; }
public List<String> getAudiences() { return audiences; }
public void setAudiences(List<String> audiences) { this.audiences = audiences; }
}
public static class CorsConfig {
private List<String> allowedMethods = List.of("GET", "POST", "PUT", "DELETE", "OPTIONS");
private List<String> allowedHeaders = List.of("*");
private boolean allowCredentials = true;
@NotNull
private Duration maxAge = Duration.ofHours(1);
// Getters and Setters
public List<String> getAllowedMethods() { return allowedMethods; }
public void setAllowedMethods(List<String> allowedMethods) { this.allowedMethods = allowedMethods; }
public List<String> getAllowedHeaders() { return allowedHeaders; }
public void setAllowedHeaders(List<String> allowedHeaders) { this.allowedHeaders = allowedHeaders; }
public boolean isAllowCredentials() { return allowCredentials; }
public void setAllowCredentials(boolean allowCredentials) { this.allowCredentials = allowCredentials; }
public Duration getMaxAge() { return maxAge; }
public void setMaxAge(Duration maxAge) { this.maxAge = maxAge; }
}
public static class RateLimitConfig {
private boolean enabled = false;
@Min(1)
private int requestsPerMinute = 100;
private List<String> excludedPaths = List.of("/health", "/metrics");
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public int getRequestsPerMinute() { return requestsPerMinute; }
public void setRequestsPerMinute(int requestsPerMinute) { this.requestsPerMinute = requestsPerMinute; }
public List<String> getExcludedPaths() { return excludedPaths; }
public void setExcludedPaths(List<String> excludedPaths) { this.excludedPaths = excludedPaths; }
}
}
public static class FeatureFlag {
@NotBlank
private String name;
private String description;
private boolean enabled = false;
private double rolloutPercentage = 0.0;
private List<String> enabledForUsers = List.of();
private Map<String, Object> parameters = Map.of();
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public double getRolloutPercentage() { return rolloutPercentage; }
public void setRolloutPercentage(double rolloutPercentage) { this.rolloutPercentage = rolloutPercentage; }
public List<String> getEnabledForUsers() { return enabledForUsers; }
public void setEnabledForUsers(List<String> enabledForUsers) { this.enabledForUsers = enabledForUsers; }
public Map<String, Object> getParameters() { return parameters; }
public void setParameters(Map<String, Object> parameters) { this.parameters = parameters; }
}
}
Dhall Integration Services
3. Dhall Parser Service
package com.example.dhall.service;
import com.example.dhall.model.ApplicationConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dhall.Context;
import org.dhall.Import;
import org.dhall.Parser;
import org.dhall.Type;
import org.dhall.core.Expr;
import org.dhall.core.constant.Type;
import org.dhall.imports.ImportException;
import org.dhall.imports.ImportMode;
import org.dhall.imports.Importer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class DhallParserService {
private static final Logger log = LoggerFactory.getLogger(DhallParserService.class);
private final ObjectMapper objectMapper;
private final ResourceLoader resourceLoader;
private final Parser parser;
private final Importer importer;
private final Map<String, Object> cache = new ConcurrentHashMap<>();
public DhallParserService(ObjectMapper objectMapper, ResourceLoader resourceLoader) {
this.objectMapper = objectMapper;
this.resourceLoader = resourceLoader;
this.parser = new Parser();
this.importer = new Importer(ImportMode.REMOTE);
}
@Cacheable(value = "dhallConfig", key = "#configPath")
public ApplicationConfig parseApplicationConfig(String configPath) throws DhallParseException {
try {
log.info("Parsing Dhall configuration from: {}", configPath);
// Parse the Dhall file
Expr<Type, Void> expression = parseDhallFile(configPath);
// Normalize the expression
Expr<Type, Void> normalized = expression.normalize();
// Convert to JSON
String json = normalized.toJsonString();
// Parse JSON into ApplicationConfig
ApplicationConfig config = objectMapper.readValue(json, ApplicationConfig.class);
log.info("Successfully parsed configuration for application: {}", config.getName());
return config;
} catch (Exception e) {
throw new DhallParseException("Failed to parse Dhall configuration: " + configPath, e);
}
}
public <T> T parseDhallToType(String configPath, Class<T> type) throws DhallParseException {
try {
log.debug("Parsing Dhall file to type {}: {}", type.getSimpleName(), configPath);
Expr<Type, Void> expression = parseDhallFile(configPath);
Expr<Type, Void> normalized = expression.normalize();
String json = normalized.toJsonString();
return objectMapper.readValue(json, type);
} catch (Exception e) {
throw new DhallParseException("Failed to parse Dhall to type " + type.getSimpleName(), e);
}
}
public Map<String, Object> parseDhallToMap(String configPath) throws DhallParseException {
try {
log.debug("Parsing Dhall file to Map: {}", configPath);
Expr<Type, Void> expression = parseDhallFile(configPath);
Expr<Type, Void> normalized = expression.normalize();
String json = normalized.toJsonString();
return objectMapper.readValue(json, Map.class);
} catch (Exception e) {
throw new DhallParseException("Failed to parse Dhall to Map", e);
}
}
public Object parseDhallExpression(String dhallExpression) throws DhallParseException {
try {
log.debug("Parsing Dhall expression: {}", dhallExpression);
Expr<Type, Void> expression = parser.parse(dhallExpression);
Expr<Type, Void> normalized = expression.normalize();
return convertDhallToJava(normalized);
} catch (Exception e) {
throw new DhallParseException("Failed to parse Dhall expression", e);
}
}
private Expr<Type, Void> parseDhallFile(String configPath) throws IOException, ImportException {
Resource resource = resourceLoader.getResource(configPath);
if (!resource.exists()) {
throw new IOException("Dhall configuration file not found: " + configPath);
}
Path filePath = Paths.get(resource.getURI());
Import fileImport = Import.local(filePath, ImportMode.REMOTE);
return parser.parse(fileImport);
}
private Object convertDhallToJava(Expr<Type, Void> expression) {
if (expression instanceof Expr.Constant) {
Expr.Constant constant = (Expr.Constant) expression;
if (constant instanceof Expr.Constant.Bool) {
return ((Expr.Constant.Bool) constant).getValue();
} else if (constant instanceof Expr.Constant.Natural) {
return ((Expr.Constant.Natural) constant).getValue();
} else if (constant instanceof Expr.Constant.Integer) {
return ((Expr.Constant.Integer) constant).getValue();
} else if (constant instanceof Expr.Constant.Double) {
return ((Expr.Constant.Double) constant).getValue();
} else if (constant instanceof Expr.Constant.Text) {
return ((Expr.Constant.Text) constant).getValue();
}
} else if (expression instanceof Expr.List) {
Expr.List list = (Expr.List) expression;
return list.getElements().stream()
.map(this::convertDhallToJava)
.toList();
} else if (expression instanceof Expr.Record) {
Expr.Record record = (Expr.Record) expression;
Map<String, Object> result = new ConcurrentHashMap<>();
record.getFields().forEach((key, value) -> {
result.put(key, convertDhallToJava(value));
});
return result;
} else if (expression instanceof Expr.Some) {
Expr.Some some = (Expr.Some) expression;
return convertDhallToJava(some.getValue());
} else if (expression instanceof Expr.None) {
return null;
}
// Fallback to JSON conversion
try {
String json = expression.toJsonString();
return objectMapper.readValue(json, Object.class);
} catch (Exception e) {
log.warn("Failed to convert Dhall expression to Java object, returning as string");
return expression.toString();
}
}
public void clearCache() {
cache.clear();
log.info("Cleared Dhall parser cache");
}
public void clearCache(String key) {
cache.remove(key);
log.debug("Cleared cache entry: {}", key);
}
public static class DhallParseException extends Exception {
public DhallParseException(String message) {
super(message);
}
public DhallParseException(String message, Throwable cause) {
super(message, cause);
}
}
}
4. Configuration Manager
package com.example.dhall.service;
import com.example.dhall.model.ApplicationConfig;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class ConfigurationManager {
private static final Logger log = LoggerFactory.getLogger(ConfigurationManager.class);
private final DhallParserService dhallParser;
@Value("${app.config.path:classpath:config.dhall}")
private String configPath;
@Value("${app.config.watch.enabled:false}")
private boolean watchEnabled;
@Value("${app.config.watch.interval:30000}")
private long watchInterval;
private ApplicationConfig currentConfig;
private long lastModified;
private final Map<String, Object> customConfigs = new ConcurrentHashMap<>();
public ConfigurationManager(DhallParserService dhallParser) {
this.dhallParser = dhallParser;
}
@PostConstruct
public void initialize() {
loadConfiguration();
if (watchEnabled) {
log.info("Configuration file watching enabled with interval: {}ms", watchInterval);
}
}
public ApplicationConfig getConfiguration() {
return currentConfig;
}
public <T> T getCustomConfiguration(String key, Class<T> type) {
Object config = customConfigs.get(key);
if (config != null && type.isInstance(config)) {
return type.cast(config);
}
return null;
}
public void loadConfiguration() {
try {
ApplicationConfig newConfig = dhallParser.parseApplicationConfig(configPath);
if (currentConfig != null && !newConfig.equals(currentConfig)) {
log.info("Configuration changed detected. Reloading...");
// Notify configuration change listeners
notifyConfigurationChange(newConfig);
}
currentConfig = newConfig;
lastModified = System.currentTimeMillis();
log.info("Configuration loaded successfully for application: {}",
currentConfig.getName());
} catch (DhallParserService.DhallParseException e) {
log.error("Failed to load configuration from: {}", configPath, e);
throw new RuntimeException("Configuration loading failed", e);
}
}
public void loadCustomConfiguration(String key, String configPath, Class<?> type) {
try {
Object config = dhallParser.parseDhallToType(configPath, type);
customConfigs.put(key, config);
log.info("Loaded custom configuration '{}' from: {}", key, configPath);
} catch (DhallParserService.DhallParseException e) {
log.error("Failed to load custom configuration '{}' from: {}", key, configPath, e);
}
}
@Scheduled(fixedRateString = "${app.config.watch.interval:30000}")
public void watchConfiguration() {
if (!watchEnabled) {
return;
}
try {
// Check if configuration file has been modified
// In a real implementation, you'd check file modification time
// For now, we'll reload based on time interval
loadConfiguration();
} catch (Exception e) {
log.error("Error during configuration watch", e);
}
}
public void reloadConfiguration() {
log.info("Manual configuration reload requested");
dhallParser.clearCache(configPath);
loadConfiguration();
}
private void notifyConfigurationChange(ApplicationConfig newConfig) {
// In a real implementation, you'd notify listeners about configuration changes
// This could be implemented using Spring events or a custom observer pattern
log.info("Configuration change detected. Application: {}, Version: {}",
newConfig.getName(), newConfig.getVersion());
}
public boolean isConfigurationLoaded() {
return currentConfig != null;
}
public String getConfigSource() {
return configPath;
}
}
5. Configuration Validator
package com.example.dhall.service;
import com.example.dhall.model.ApplicationConfig;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.stream.Collectors;
@Component
public class ConfigurationValidator {
private static final Logger log = LoggerFactory.getLogger(ConfigurationValidator.class);
private final Validator validator;
public ConfigurationValidator() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
this.validator = factory.getValidator();
}
}
public ValidationResult validate(ApplicationConfig config) {
Set<ConstraintViolation<ApplicationConfig>> violations = validator.validate(config);
if (violations.isEmpty()) {
log.info("Configuration validation passed");
return new ValidationResult(true, "Configuration is valid");
} else {
String errorMessage = violations.stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.collect(Collectors.joining(", "));
log.warn("Configuration validation failed: {}", errorMessage);
return new ValidationResult(false, errorMessage);
}
}
public <T> ValidationResult validate(T config, Class<?>... groups) {
Set<ConstraintViolation<T>> violations = validator.validate(config, groups);
if (violations.isEmpty()) {
return new ValidationResult(true, "Configuration is valid");
} else {
String errorMessage = violations.stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.collect(Collectors.joining(", "));
return new ValidationResult(false, errorMessage);
}
}
public static class ValidationResult {
private final boolean valid;
private final String message;
public ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public boolean isValid() { return valid; }
public String getMessage() { return message; }
public void throwIfInvalid() {
if (!valid) {
throw new ConfigurationValidationException(message);
}
}
}
public static class ConfigurationValidationException extends RuntimeException {
public ConfigurationValidationException(String message) {
super(message);
}
}
}
Spring Boot Integration
6. Configuration Properties
package com.example.dhall.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app.dhall")
public class DhallConfigurationProperties {
private String configPath = "classpath:config.dhall";
private boolean watchEnabled = false;
private long watchInterval = 30000;
private boolean validationEnabled = true;
private boolean cacheEnabled = true;
private String cacheName = "dhallConfig";
// Getters and Setters
public String getConfigPath() { return configPath; }
public void setConfigPath(String configPath) { this.configPath = configPath; }
public boolean isWatchEnabled() { return watchEnabled; }
public void setWatchEnabled(boolean watchEnabled) { this.watchEnabled = watchEnabled; }
public long getWatchInterval() { return watchInterval; }
public void setWatchInterval(long watchInterval) { this.watchInterval = watchInterval; }
public boolean isValidationEnabled() { return validationEnabled; }
public void setValidationEnabled(boolean validationEnabled) { this.validationEnabled = validationEnabled; }
public boolean isCacheEnabled() { return cacheEnabled; }
public void setCacheEnabled(boolean cacheEnabled) { this.cacheEnabled = cacheEnabled; }
public String getCacheName() { return cacheName; }
public void setCacheName(String cacheName) { this.cacheName = cacheName; }
}
7. Spring Configuration
package com.example.dhall.config;
import com.example.dhall.model.ApplicationConfig;
import com.example.dhall.service.ConfigurationManager;
import com.example.dhall.service.ConfigurationValidator;
import com.example.dhall.service.DhallParserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableConfigurationProperties(DhallConfigurationProperties.class)
@EnableScheduling
public class DhallAutoConfiguration {
@Bean
public DhallParserService dhallParserService(ObjectMapper objectMapper) {
return new DhallParserService(objectMapper, null);
}
@Bean
public ConfigurationValidator configurationValidator() {
return new ConfigurationValidator();
}
@Bean
public ConfigurationManager configurationManager(
DhallParserService dhallParserService,
DhallConfigurationProperties properties) {
ConfigurationManager manager = new ConfigurationManager(dhallParserService);
// Properties would be set via @Value or constructor injection in real implementation
return manager;
}
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("dhallConfig");
}
@Bean
public ApplicationConfig applicationConfig(ConfigurationManager configurationManager) {
return configurationManager.getConfiguration();
}
}
Example Dhall Configuration Files
8. Main Configuration File
-- config.dhall
let ApplicationConfig =
{ name : Text
, version : Text
, server :
{ port : Natural
, host : Text
, timeout : Text
, maxThreads : Natural
, sslEnabled : Bool
, ssl :
Optional
{ keyStore : Text
, keyStorePassword : Text
, keyAlias : Optional Text
, protocols : List Text
}
}
, database :
{ url : Text
, username : Text
, password : Text
, driverClassName : Text
, pool :
{ minSize : Natural
, maxSize : Natural
, connectionTimeout : Text
, idleTimeout : Text
, maxLifetime : Text
}
, properties : List { mapKey : Text, mapValue : Text }
}
, cache :
Optional
{ enabled : Bool
, defaultTtl : Text
, redis :
Optional
{ host : Text
, port : Natural
, password : Optional Text
, database : Natural
, timeout : Text
}
, local :
Optional
{ maxSize : Natural
, ttl : Text
}
}
, security :
Optional
{ enabled : Bool
, jwt :
Optional
{ secret : Text
, expiration : Text
, issuer : Text
, audiences : List Text
}
, cors :
Optional
{ allowedMethods : List Text
, allowedHeaders : List Text
, allowCredentials : Bool
, maxAge : Text
}
, rateLimit :
Optional
{ enabled : Bool
, requestsPerMinute : Natural
, excludedPaths : List Text
}
, allowedOrigins : List Text
}
, featureFlags : List
{ name : Text
, description : Text
, enabled : Bool
, rolloutPercentage : Double
, enabledForUsers : List Text
, parameters : List { mapKey : Text, mapValue : Text }
}
, customSettings : List { mapKey : Text, mapValue : Text }
}
let serverConfig =
{ port = 8080
, host = "0.0.0.0"
, timeout = "PT30S"
, maxThreads = 50
, sslEnabled = False
, ssl = None {
keyStore : Text
, keyStorePassword : Text
, keyAlias : Optional Text
, protocols : List Text
}
}
let dbConfig =
{ url = "jdbc:postgresql://localhost:5432/myapp"
, username = "app_user"
, password = "secret"
, driverClassName = "org.postgresql.Driver"
, pool =
{ minSize = 5
, maxSize = 20
, connectionTimeout = "PT30S"
, idleTimeout = "PT10M"
, maxLifetime = "PT30M"
}
, properties =
[ { mapKey = "useSSL", mapValue = "false" }
, { mapKey = "serverTimezone", mapValue = "UTC" }
]
}
let cacheConfig =
Some
{ enabled = True
, defaultTtl = "PT30M"
, redis =
Some
{ host = "localhost"
, port = 6379
, password = None Text
, database = 0
, timeout = "PT10S"
}
, local =
Some
{ maxSize = 1000
, ttl = "PT10M"
}
}
let securityConfig =
Some
{ enabled = True
, jwt =
Some
{ secret = "my-secret-key"
, expiration = "PT24H"
, issuer = "myapp"
, audiences = [ "web", "mobile" ]
}
, cors =
Some
{ allowedMethods = [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ]
, allowedHeaders = [ "*" ]
, allowCredentials = True
, maxAge = "PT1H"
}
, rateLimit =
Some
{ enabled = False
, requestsPerMinute = 100
, excludedPaths = [ "/health", "/metrics" ]
}
, allowedOrigins = [ "*" ]
}
let featureFlags =
[ { name = "new-ui"
, description = "Enable new user interface"
, enabled = True
, rolloutPercentage = 50.0
, enabledForUsers = [ "user1", "user2" ]
, parameters = [] : List { mapKey : Text, mapValue : Text }
}
, { name = "experimental-api"
, description = "Enable experimental API endpoints"
, enabled = False
, rolloutPercentage = 10.0
, enabledForUsers = [] : List Text
, parameters =
[ { mapKey = "rateLimit", mapValue = "100" }
, { mapKey = "timeout", mapValue = "PT10S" }
]
}
]
let customSettings =
[ { mapKey = "logging.level", mapValue = "INFO" }
, { mapKey = "metrics.enabled", mapValue = "true" }
, { mapKey = "feature.toggle.admin", mapValue = "false" }
]
in { name = "My Application"
, version = "1.0.0"
, server = serverConfig
, database = dbConfig
, cache = cacheConfig
, security = securityConfig
, featureFlags = featureFlags
, customSettings = customSettings
}
9. Environment-specific Configuration
-- config/production.dhall
let base = ./base.dhall
let productionOverrides =
{ server =
base.server ⫽ { port = 443, sslEnabled = True }
, database =
base.database ⫽
{ url = "jdbc:postgresql://prod-db:5432/myapp"
, password = "prod-secret-password"
}
, cache =
Some
( merge
( base.cache ? { enabled = True } )
{ redis =
Some
( merge
( base.cache.redis ? { host = "localhost", port = 6379 } )
{ host = "redis-cluster.prod.svc"
, password = Some "redis-prod-password"
}
)
}
)
, security =
Some
( merge
( base.security ? { enabled = True } )
{ rateLimit =
Some
( merge
( base.security.rateLimit ? { enabled = False } )
{ enabled = True, requestsPerMinute = 1000 }
)
}
)
}
in base ⫽ productionOverrides
10. Feature Flags Configuration
-- features.dhall
let FeatureFlag =
{ name : Text
, description : Text
, enabled : Bool
, rolloutPercentage : Double
, enabledForUsers : List Text
, parameters : List { mapKey : Text, mapValue : Text }
}
let features =
[ { name = "dark-mode"
, description = "Enable dark mode theme"
, enabled = True
, rolloutPercentage = 100.0
, enabledForUsers = [] : List Text
, parameters = [] : List { mapKey : Text, mapValue : Text }
}
, { name = "payment-gateway-v2"
, description = "Use new payment gateway"
, enabled = False
, rolloutPercentage = 25.0
, enabledForUsers = [ "premium-user-1", "premium-user-2" ]
, parameters =
[ { mapKey = "timeout", mapValue = "PT30S" }
, { mapKey = "retryCount", mapValue = "3" }
]
}
, { name = "ai-recommendations"
, description = "Enable AI-powered recommendations"
, enabled = True
, rolloutPercentage = 50.0
, enabledForUsers = [] : List Text
, parameters =
[ { mapKey = "modelVersion", mapValue = "v2" }
, { mapKey = "confidenceThreshold", mapValue = "0.8" }
]
}
]
in features
REST API Controllers
11. Configuration Management API
package com.example.dhall.controller;
import com.example.dhall.model.ApplicationConfig;
import com.example.dhall.service.ConfigurationManager;
import com.example.dhall.service.ConfigurationValidator;
import com.example.dhall.service.DhallParserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/config")
public class ConfigurationController {
private final ConfigurationManager configManager;
private final ConfigurationValidator validator;
private final DhallParserService dhallParser;
public ConfigurationController(
ConfigurationManager configManager,
ConfigurationValidator validator,
DhallParserService dhallParser) {
this.configManager = configManager;
this.validator = validator;
this.dhallParser = dhallParser;
}
@GetMapping
public ResponseEntity<ApplicationConfig> getConfiguration() {
ApplicationConfig config = configManager.getConfiguration();
if (config != null) {
return ResponseEntity.ok(config);
} else {
return ResponseEntity.notFound().build();
}
}
@PostMapping("/reload")
public ResponseEntity<Map<String, String>> reloadConfiguration() {
configManager.reloadConfiguration();
return ResponseEntity.ok(Map.of(
"status", "reloaded",
"message", "Configuration reloaded successfully"
));
}
@GetMapping("/validate")
public ResponseEntity<Map<String, Object>> validateConfiguration() {
ApplicationConfig config = configManager.getConfiguration();
ConfigurationValidator.ValidationResult result = validator.validate(config);
return ResponseEntity.ok(Map.of(
"valid", result.isValid(),
"message", result.getMessage(),
"timestamp", System.currentTimeMillis()
));
}
@PostMapping("/parse")
public ResponseEntity<Map<String, Object>> parseDhallExpression(
@RequestBody ParseRequest request) {
try {
Object result = dhallParser.parseDhallExpression(request.getExpression());
return ResponseEntity.ok(Map.of(
"result", result,
"type", result != null ? result.getClass().getSimpleName() : "null"
));
} catch (DhallParserService.DhallParseException e) {
return ResponseEntity.badRequest().body(Map.of(
"error", e.getMessage()
));
}
}
@GetMapping("/source")
public ResponseEntity<Map<String, String>> getConfigSource() {
return ResponseEntity.ok(Map.of(
"source", configManager.getConfigSource(),
"loaded", String.valueOf(configManager.isConfigurationLoaded())
));
}
@PostMapping("/cache/clear")
public ResponseEntity<Map<String, String>> clearCache() {
dhallParser.clearCache();
return ResponseEntity.ok(Map.of(
"status", "success",
"message", "Cache cleared successfully"
));
}
// Request DTO
public static class ParseRequest {
private String expression;
public String getExpression() { return expression; }
public void setExpression(String expression) { this.expression = expression; }
}
}
Usage Examples
12. Using Configuration in Services
package com.example.dhall.service;
import com.example.dhall.model.ApplicationConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class ApplicationService {
private static final Logger log = LoggerFactory.getLogger(ApplicationService.class);
private final ApplicationConfig config;
private final ConfigurationManager configManager;
public ApplicationService(ApplicationConfig config, ConfigurationManager configManager) {
this.config = config;
this.configManager = configManager;
log.info("Initializing service with configuration: {} v{}",
config.getName(), config.getVersion());
}
public void start() {
log.info("Starting application service on {}:{}",
config.getServer().getHost(), config.getServer().getPort());
// Use configuration values
if (config.getSecurity() != null && config.getSecurity().isEnabled()) {
log.info("Security features enabled");
}
if (config.getCache() != null && config.getCache().isEnabled()) {
log.info("Cache enabled with TTL: {}", config.getCache().getDefaultTtl());
}
}
public boolean isFeatureEnabled(String featureName) {
return config.getFeatureFlags().stream()
.filter(flag -> flag.getName().equals(featureName))
.findFirst()
.map(flag -> flag.isEnabled())
.orElse(false);
}
public double getFeatureRolloutPercentage(String featureName) {
return config.getFeatureFlags().stream()
.filter(flag -> flag.getName().equals(featureName))
.findFirst()
.map(flag -> flag.getRolloutPercentage())
.orElse(0.0);
}
}
Testing
13. Unit Tests
package com.example.dhall.test;
import com.example.dhall.model.ApplicationConfig;
import com.example.dhall.service.DhallParserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ResourceLoader;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class DhallParserServiceTest {
@Test
void testParseApplicationConfig() throws Exception {
// Given
ObjectMapper objectMapper = new ObjectMapper();
ResourceLoader resourceLoader = mock(ResourceLoader.class);
DhallParserService parser = new DhallParserService(objectMapper, resourceLoader);
// When
ApplicationConfig config = parser.parseApplicationConfig("classpath:test-config.dhall");
// Then
assertThat(config).isNotNull();
assertThat(config.getName()).isEqualTo("Test Application");
assertThat(config.getServer().getPort()).isEqualTo(8080);
assertThat(config.getDatabase().getUrl()).isNotEmpty();
}
@Test
void testParseDhallToMap() throws Exception {
// Given
ObjectMapper objectMapper = new ObjectMapper();
ResourceLoader resourceLoader = mock(ResourceLoader.class);
DhallParserService parser = new DhallParserService(objectMapper, resourceLoader);
// When
Map<String, Object> result = parser.parseDhallToMap("classpath:simple-config.dhall");
// Then
assertThat(result).isNotNull();
assertThat(result.get("name")).isEqualTo("Simple App");
}
}
Configuration
14. Application Properties
# application.yml app: dhall: config-path: "classpath:config.dhall" watch-enabled: true watch-interval: 30000 validation-enabled: true cache-enabled: true cache-name: "dhallConfig" spring: cache: type: simple cache-names: dhallConfig logging: level: com.example.dhall: INFO
Best Practices
- Type Safety: Leverage Dhall's type system to catch configuration errors early
- Modularity: Break down configuration into smaller, reusable files
- Environment-specific: Use different configuration files for different environments
- Validation: Always validate configuration after parsing
- Caching: Cache parsed configurations for better performance
- Monitoring: Monitor configuration changes and reload when necessary
- Documentation: Document configuration structure and options
- Security: Never commit sensitive information; use environment variables or secrets
Conclusion
This comprehensive Dhall configuration integration provides:
- Type-safe Configuration: Leverage Dhall's strong type system
- Java Integration: Seamless integration with Spring Boot applications
- Validation: Comprehensive configuration validation
- Caching: Efficient caching of parsed configurations
- Hot Reloading: Support for configuration changes without restart
- Modular Design: Support for modular configuration files
- REST API: Management API for configuration inspection and control
The integration enables safe, flexible, and maintainable configuration management for Java applications while leveraging Dhall's powerful features like type safety, imports, and functions.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.