How the DD Poker Property Configuration System Works with User Overrides
The DD Poker property configuration system loads hierarchical .properties files and merges per-user overrides from module/override/<username>.properties when allowOverrides is enabled, with later loads overwriting earlier keys to give user settings highest precedence.
DD Poker implements a sophisticated property configuration system in its com.donohoedigital.config package that allows individual users to override default application settings without modifying shared configuration files. This mechanism, centered around the PropertyConfig class, enables personalized gameplay experiences while maintaining sane defaults for all users.
Initialization and the Loading Sequence
The configuration loading process begins when ConfigManager instantiates PropertyConfig during application startup. The constructor signature determines whether user overrides are permitted:
// Inside ConfigManager constructor
new PropertyConfig(sAppName, modules, type, locale, allowOverrides);
Source: [ConfigManager.java](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/ConfigManager.java)
The Module Iteration Loop
Inside PropertyConfig.init(), the loader iterates over each module (such as "common" or "poker") and loads configuration files in a specific sequence. This loop establishes the base configuration before applying any user-specific modifications:
for (String module : modules) {
file = new File(module, PROPS_CONFIG_COMMON);
loadURL(file, sLocale, false); // defaults
// type-specific file
file = new File(module, name);
loadURL(file, sLocale, false);
// per-user override
if (allowOverrides) {
String user = ConfigUtils.getUserName();
file = new File(module + "/override/" + user.toLowerCase() + ".properties");
loadURL(file, sLocale, true);
}
}
Source: [PropertyConfig.java](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/PropertyConfig.java)
Per-User Override Resolution
When the allowOverrides parameter is true, the system calls ConfigUtils.getUserName() to determine the current operating system user. It then constructs a file path following the pattern module/override/<username>.properties (with the username converted to lowercase). The loadURL method receives true for its bOverride parameter, triggering log messages that indicate local overrides are being applied.
Override Precedence and Merging Strategy
The system employs a last-write-wins merging strategy using standard java.util.Properties.load() behavior. Configuration sources are loaded in the following precedence order, with each subsequent source overwriting existing keys:
- Base defaults:
common.propertiesfor each module - Application-type specific: Files like
client.propertiesorserver.properties - Per-user overrides:
module/override/<username>.properties(loaded only whenallowOverridesis true) - Data-directory overrides: An optional
testing.propertiesfile in the user's client home directory
Classpath Resolution and Localization
The loadURL method resolves resources using the pattern classpath*:config/<path>. When a locale is specified, the loader first searches for localized variants (e.g., client.properties.fr) before falling back to the base file. This resolution occurs before the override logic determines whether to apply user-specific values.
Testing Overrides for Development
After the module loop completes, the system checks for ad-hoc overrides intended for unit tests or development tweaks:
RuntimeDirectory dir = new DefaultRuntimeDirectory();
File userdir = dir.getClientHome(appName);
File override = new File(userdir, "testing.properties");
if (override.exists()) {
load(override); // forces the testing file on top
}
Source: [PropertyConfig.java](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/PropertyConfig.java) and [DefaultRuntimeDirectory.java](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/DefaultRuntimeDirectory.java)
Accessing Merged Configuration Values
Once initialization completes, the merged Properties object resides in a singleton propConfig instance. All application code accesses values through static helper methods that provide type conversion and default value handling:
String maxPlayers = PropertyConfig.getStringProperty("game.maxplayers", "8");
int timeoutSec = PropertyConfig.getIntegerProperty("network.timeout", 30);
boolean debug = PropertyConfig.getBooleanProperty("debug.enabled", false);
These methods read from the already-merged configuration, ensuring that user overrides are transparent to the calling code.
Practical Example: Creating a User Override
Consider the default configuration in poker/client.properties:
game.maxplayers=8
A user named Alice wishes to increase the maximum to 12 seats. She creates the file poker/override/alice.properties with the following content:
game.maxplayers=12
When the application starts with new ConfigManager("poker", ApplicationType.CLIENT, true), the loading sequence executes as follows:
- Loads
poker/common.propertiesandpoker/client.properties, settinggame.maxplayersto8 - Detects
allowOverrides=trueand locatespoker/override/alice.properties - Calls
loadURL(..., true), logging the override application - The key
game.maxplayersis overwritten to12
Subsequent calls to PropertyConfig.getIntegerProperty("game.maxplayers", ...) return 12 for Alice, while other users continue to receive the default value of 8.
Summary
- The property configuration system loads base files (
common.properties, type-specific files) for each module before checking for user overrides. - When
allowOverridesis enabled, the system loads per-user files frommodule/override/<username>.properties, with usernames normalized to lowercase viaConfigUtils.getUserName(). - The standard
java.util.Properties.load()mechanism ensures that later configuration sources overwrite earlier ones, establishing a clear precedence hierarchy. - An optional
testing.propertiesfile in the client home directory can supersede all other values for development purposes. - Runtime code accesses the merged configuration through static methods like
getStringProperty()andgetIntegerProperty()without needing to understand the override hierarchy.
Frequently Asked Questions
How does the system determine which user override file to load?
The system calls ConfigUtils.getUserName() to obtain the current operating system username, converts it to lowercase, and appends .properties to construct the filename. It then searches for this file within each module's override directory using the path pattern module/override/<username>.properties.
Can user overrides modify any configuration property?
Yes, the property configuration system treats all keys equally during the merge process. Any property defined in the base configuration files can be overwritten in a per-user override file, including network timeouts, game settings, or debug flags.
What happens if the user override file does not exist?
If the file does not exist, loadURL() simply returns without error, and the system continues using the values loaded from the default configuration files. This allows allowOverrides to remain enabled globally without requiring every user to create an override file.
How do localized property files interact with user overrides?
Localization occurs at the loadURL level before override logic applies. For a given base file, the system first attempts to load the locale-specific variant (e.g., client.properties.fr). The per-user override file then loads on top of whatever localized version was resolved, allowing users to override both default and localized values.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →