INI file support

##### Table of Contents

## 1 - Overview

The {@link org.apache.juneau.ini.ConfigFileBuilder} and {@link org.apache.juneau.ini.ConfigFile} classes implement an API for working with INI-style configuration files such as the following:

#-------------------------- # Default section #-------------------------- key1 = 1 key2 = true key3 = 1,2,3 key4 = http://foo #-------------------------- # A comment about Section 1 #-------------------------- [Section1] key1 = 2 key2 = false key3 = 4,5,6 key4 = http://bar

The {@link org.apache.juneau.ini.ConfigFileBuilder} class is used to instantiate instances of {@link org.apache.juneau.ini.ConfigFile} which can then be used to retrieve config file values through either "key" or "Section/key" identifiers.

int key1; boolean key2; int[] key3; URL key4; // Get our config file using the default config manager ConfigFile f = ConfigFile.create().build("C:/temp/MyConfig.cfg"); // Read values from default section key1 = f.getInt("key1"); key2 = f.getBoolean("key2"); key3 = f.getObject(int[].class, "key3"); key4 = f.getObject(URL.class, "key4"); // Read values from Section #1 key1 = f.getInt("Section1/key1"); key2 = f.getBoolean("Section1/key2"); key3 = f.getObject(int[].class, "Section1/key3"); key4 = f.getObject(URL.class, "Section1/key4");

The interface also allows config files to be constructed programmatically...

// Construct the sample INI file programmatically ConfigFile f = ConfigFile.create().build("C:/temp/MyConfig.cfg", true) .addLines(null, // The default 'null' section "# Default section", // A regular comment "key1 = 1", // A numeric entry "key2 = true", // A boolean entry "key3 = 1,2,3", // An array entry "key4 = http://foo", // A POJO entry "") // A blank line .addHeaderComments("Section1", // The 'Section1' section "A comment about Section 1") // A header comment .addLines("Section1", // The 'Section1' section "key1 = 2", // A numeric entry "key2 = false", // A boolean entry "key3 = 4,5,6", // An array entry "key4 = http://bar") // A POJO entry .save(); // Save to MyConfig.cfg

The following is equivalent, except uses {@link org.apache.juneau.ini.ConfigFile#put(String,Object)} to set values. Note how we're setting values as POJOs which will be automatically converted to strings when persisted to disk.

// Construct the sample INI file programmatically ConfigFile f = ConfigFile.create().build("C:/temp/MyConfig.cfg", true) .addLines(null, "# Default section") .addHeaderComments("Section1", "A comment about Section 1"); cf.put("key1", 1); cf.put("key2", true); cf.put("key3", new int[]{1,2,3}); cf.put("key4", new URL("http://foo")); cf.put("Section1/key1", 2); cf.put("Section1/key2", false); cf.put("Section1/key3", new int[]{4,5,6}); cf.put("Section1/key4", new URL("http://bar")); cf.save();

Refer to {@link org.apache.juneau.ini.ConfigFile#put(String,Object,boolean)} for a description of formats for various data types.

Various convenience getter methods are provided for retrieving different data types:

// Strings with default values // key1 = foobar String key1 = cf.getString("key1"); // Numbers // key2 = 123 float key2 = cf.getObject(float.class, "key2"); // Booleans // key3 = true boolean key3 = cf.getBoolean("key3"); // Objects convertable to and from strings using the JSON serializer and parser // key4 = http://foo URL key4 = cf.getObject(URL.class, "key4"); // Arrays of strings // key5 = foo, bar String[] key5 = cf.getStringArray("key5"); // Arrays of objects // key6 = http://foo,http://bar URL[] key6 = cf.getObject(URL[].class, "key6"); // Arrays of primitives // key7 = 1,2,3 int[] key7 = cf.getObject(int[].class, "key7"); // Enums // key8 = MINUTES TimeUnit key8 = cf.getObject(TimeUnit.class, "key8"); // Beans // key9 = {name:'John Smith', addresses:[{street:'101 Main St', city:'Anywhere', state:'TX'}]} Person key9 = cf.getObject(Person.class, "key9"); // Generic Maps // key10 = {foo:'bar', baz:123} Map key10 = cf.getObject(ObjectMap.class, "key10");

## 2 - Variables

Config files can contain variables that get resolved dynamically using the {@link org.apache.juneau.svl.VarResolver} API.

Resolving config files can be retrieved through the following methods:

- {@link org.apache.juneau.ini.ConfigFile#getResolving()} - Returns a config file that resolves a default set of variables.
- {@link org.apache.juneau.ini.ConfigFile#getResolving(VarResolver)} - Returns a config file that resolves a custom set of variables.

The default {@link org.apache.juneau.ini.ConfigFile#getResolving()} method returns a config file that resolves the following variables:

- `$S{key}`, `$S{key,default}` - System properties.
- `$E{key}`, `$E{key,default}` - Environment variables.
- `$C{key}`, `$C{key,default}` - Values in this configuration file.

###### Examples:

#-------------------------- # Examples #-------------------------- [MyProperties] javaHome = $S{java.home} path = $E{PATH} customMessage = Java home is $C{MyProperties/javaHome} and the environment path is $C{MyProperties/path}.

Support for variables is extensible. You can add support for your own variables by implementing custom {@link org.apache.juneau.svl.VarResolver VarResolvers}.
For example, the microservice `Resource` class provides access to config files that can contain any of the following variables:

- `$C` - Config variables.
- `$S` - System properties.
- `$E` - Environment variables.
- `$I` - Servlet init parameters.
- `$ARG` - JVM command-line arguments.
- `$MF` - Main jar manifest file entries.
- `$L` - Localized strings.
- `$A` - HTTP request attributes.
- `$P` - HTTP request URL parameters.
- `$R` - HTTP request variables.
- `$UE` - URL-encoding function.

## 3 - Encoded Entries

If a config file contains sensitive information such as passwords, those values can be marked for encoding by appending '*' to the end of the key name.
If a marked and unencoded value is detected in the file during load, it will be encoded and saved immediately.

For example, the following password is marked for encoding....

[MyHost] url = http://localhost:9080/foo user = me password* = mypassword

After initial loading, the file contents will contain an encoded value...

[MyHost] url = http://localhost:9080/foo user = me password* = {AwwJVhwUQFZEMg==}

The default encoder is {@link org.apache.juneau.ini.XorEncoder} which is a simple XOR+Base64 encoder.
If desired, custom encoder can be used by implementing the {@link org.apache.juneau.ini.Encoder} interface and creating your own `ConfigFileBuilder` using the {@link org.apache.juneau.ini.ConfigFileBuilder#encoder(Encoder)} method.

## 4 - Listeners

The following method is provided for listening to changes made on config files:

{@link org.apache.juneau.ini.ConfigFile#addListener(ConfigFileListener)}.

Subclasses are provided for listening for different kinds of events:

- {@link org.apache.juneau.ini.ConfigFileListener} - Config file is saved, loaded, or modified.
- {@link org.apache.juneau.ini.SectionListener} - One or more entries in a section are modified.
- {@link org.apache.juneau.ini.EntryListener} - An individual entry is modified.

###### Example:

// Get our config file using the default config manager ConfigFile f = ConfigFile.create().build("C:/temp/MyConfig.cfg"); // Add a listener for an entry f.addListener( new EntryListener("Section1/key1") { @Override public void onChange(ConfigFile cf) { System.err.println("Entry changed! New value is " + cf.getString("Section1/key1")); } } );

## 5 - Command Line API

The {@link org.apache.juneau.ini.ConfigFileBuilder} class contains a {@link org.apache.juneau.ini.ConfigFileBuilder#main(String[])} method that can be used to work with config files through a command-line prompt.
This is invoked as a normal Java command:

java -jar juneau.jar org.apache.juneau.ini.ConfigFileBuilder [args]

Arguments can be any of the following...

- No arguments
  Prints usage message.
- `createBatchEnvFile -configfile <configFile> -envfile <batchFile> [-verbose]`
  Creates a batch file that will set each config file entry as an environment variable.
  Characters in the keys that are not valid as environment variable names (e.g. '/' and '.') will be converted to underscores.
- `createShellEnvFile -configFile <configFile> -envFile <configFile> [-verbose]` Creates a shell script that will set each config file entry as an environment variable.
  Characters in the keys that are not valid as environment variable names (e.g. '/' and '.') will be converted to underscores.
- `setVals -configFile <configFile> -vals [var1=val1 [var2=val2...]] [-verbose]` Sets values in config files.

For example, the following command will create the file `'MyConfig.bat'` from the contents of the file `'MyConfig.cfg'`.

java org.apache.juneau.ini.ConfigFileBuilder createBatchEnvFile -configfile C:\foo\MyConfig.cfg -batchfile C:\foo\MyConfig.bat

## 6 - Serializing Config Files

Instances of {@link org.apache.juneau.ini.ConfigFile} are POJOs that can be serialized to and parsed from all supported Juneau languages.

The `org.apache.juneau.microservice.resources.ConfigResource` is a predefined REST interface that allows access to the config file used by a microservice.
The `juneau-examples-rest` project is a microservice that includes this resource at `http://localhost:10000/sample/config`.
The sample microservice uses the following config file `juneau-examples.cfg`:

#================================================================================ # Basic configuration file for SaaS microservices # Subprojects can use this as a starting point. #================================================================================ #================================================================================ # REST settings #================================================================================ [REST] # The HTTP port number to use. # Default is Rest-Port setting in manifest file, or 8000. port = 10000 # A JSON map of servlet paths to servlet classes. # Example: # resourceMap = {'/*':'com.foo.MyServlet'} # Either resourceMap or resources must be specified. resourceMap = # A comma-delimited list of names of classes that extend from Servlet. # Resource paths are pulled from @RestResource.path() annotation, or # "/*" if annotation not specified. # Example: # resources = com.foo.MyServlet # Default is Rest-Resources in manifest file. # Either resourceMap or resources must be specified. resources = # The context root of the Jetty server. # Default is Rest-ContextPath in manifest file, or "/". contextPath = # Authentication: NONE, BASIC. authType = NONE # The BASIC auth username. # Default is Rest-LoginUser in manifest file. loginUser = # The BASIC auth password. # Default is Rest-LoginPassword in manifest file. loginPassword = # The BASIC auth realm. # Default is Rest-AuthRealm in manifest file. authRealm = # Stylesheet to use for HTML views. # The default options are: # - styles/juneau.css # - styles/devops.css # Other stylesheets can be referenced relative to the servlet package or working # directory. stylesheet = styles/devops.css # What to do when the config file is saved. # Possible values: # NOTHING - Don't do anything. # RESTART_SERVER - Restart the Jetty server. # RESTART_SERVICE - Shutdown and exit with code '3'. saveConfigAction = RESTART_SERVER # Enable SSL support. useSsl = false #================================================================================ # Bean properties on the org.eclipse.jetty.util.ssl.SslSocketFactory class #-------------------------------------------------------------------------------- # Ignored if REST/useSsl is false. #================================================================================ [REST-SslContextFactory] keyStorePath = client_keystore.jks keyStorePassword* = {HRAaRQoT} excludeCipherSuites = TLS_DHE.*, TLS_EDH.* excludeProtocols = SSLv3 allowRenegotiate = false #================================================================================ # Logger settings # See FileHandler Java class for details. #================================================================================ [Logging] # The directory where to create the log file. # Default is "." logDir = logs # The name of the log file to create for the main logger. # The logDir and logFile make up the pattern that's passed to the FileHandler # constructor. # If value is not specified, then logging to a file will not be set up. logFile = microservice.%g.log # Whether to append to the existing log file or create a new one. # Default is false. append = # The SimpleDateFormat format to use for dates. # Default is "yyyy.MM.dd hh:mm:ss". dateFormat = # The log message format. # The value can contain any of the following variables: # {date} - The date, formatted per dateFormat. # {class} - The class name. # {method} - The method name. # {logger} - The logger name. # {level} - The log level name. # {msg} - The log message. # {threadid} - The thread ID. # {exception} - The localized exception message. # Default is "[{date} {level}] {msg}%n". format = # The maximum log file size. # Suffixes available for numbers. # See ConfigFile.getInt(String,int) for details. # Default is 1M. limit = 10M # Max number of log files. # Default is 1. count = 5 # Default log levels. # Keys are logger names. # Values are serialized Level POJOs. levels = { org.apache.juneau:'INFO' } # Only print unique stack traces once and then refer to them by a simple 8 character hash identifier. # Useful for preventing log files from filling up with duplicate stack traces. # Default is false. useStackTraceHashes = true # The default level for the console logger. # Default is WARNING. consoleLevel = #================================================================================ # System properties #-------------------------------------------------------------------------------- # These are arbitrary system properties that are set during startup. #================================================================================ [SystemProperties] # Configure Jetty for StdErrLog Logging org.eclipse.jetty.util.log.class = org.eclipse.jetty.util.log.StrErrLog # Jetty logging level org.eclipse.jetty.LEVEL = WARN

The config file looks deceivingly simple. However, it should be noticed that the config file is a VERY powerful feature with many capabilities including:

When you point your browser to this resource, you'll notice that the contents of the config file are being serialized to HTML as a POJO:

Likewise, the config file can also be serialized as any of the supported languages such as JSON:

The code for implementing this page could not be any simpler, since it simply returns the config file returned by the `RestServlet.getConfig()` method.

/** * [GET /] - Show contents of config file. * * @return The config file. * @throws Exception */ @RestMethod(name=GET, path="/", description="Show contents of config file.") public ConfigFile getConfigContents() throws Exception { return getConfig(); }

The edit page takes you to an editor that allows you to modify the contents of the config file:

This latter page uses the {@link org.apache.juneau.ini.ConfigFile#toString()} method to retrieve the contents of the config file in INI format.

Since config files are serializable, that mean they can also be retrieved through the `RestClient` API.

// Create a new REST client with JSON support RestClient c = RestClient.create().build(); // Retrieve config file through REST interface ConfigFile cf = c.doGet("http://localhost:10000/sample/config").getResponse(ConfigFileImpl.class);

## 7 - Merging Config Files

In the previous example, an edit page was shown that allows you to edit config files through a REST interface.
Note that if only a single entry is modified in the config file, we only want to trigger listeners for that change, not trigger all listeners.
This is where the {@link org.apache.juneau.ini.ConfigFile#merge(ConfigFile)} method comes into play.
This method will copy the contents of one config file over to another config file, but only trigger listeners when the values are different.

The edit page is implemented with this method which is a simple PUT with the contents of the new INI file as the body of the HTTP request:

/** * [PUT /] - Sets contents of config file. * * @param contents The new contents of the config file. * @return The new config file contents. * @throws Exception */ @RestMethod(name=PUT, path="/", description="Sets contents of config file.", parameters={ @Parameter(in="body", description="New contents in INI file format.") } ) public ConfigFile setConfigContents(@Body Reader contents) throws Exception { // Create a new in-memory config file based on the contents of the HTTP request. ConfigFile cf2 = new ConfigFileBuilder.build().load(contents); // Merge the in-memory config file into the existing config file and save it. // Then return the modified config file to be parsed as a POJO. return getConfig().merge(cf2).save(); }