.. _kotlin-api:
.. default-domain:: kotlin
.. meta::
:description: CFG configuration format Kotlin / Java API
:keywords: CFG, configuration, JVM, Java, Kotlin
The CFG API for Kotlin / Java
-----------------------------
The CFG reference implementation for the Java Virtual Machine (JVM) is written in the
`Kotlin `_ programming language. The implementation assumes a
JVM version of 8 or later. The Kotlin version tested with is 1.3.61 or later. The jar
is usually named ``config-X.Y.Z.jar`` where ``X.Y.Z`` is the version number.
Installation
++++++++++++
You can install the jar from the Maven repository at
https://dl.bintray.com/reddove/public and configure Maven, Gradle etc.
appropriately. The jar will be located at
https://repo1.maven.org/maven2/com/red-dove/config/X.Y.Z/config-X.Y.Z.jar
There's a minimal example of a program and project that uses CFG `here
`_.
Exploration
+++++++++++
To explore CFG functionality for the JVM, we use the Kotlin compiler in
interactive mode to act as a Read-Eval-Print-Loop (REPL). Once installed, you
can invoke a shell using
.. code-block:: shell
$ export CP=config-X.Y.Z.jar:commons-math3-3.6.1.jar:kotlin-reflect-1.3.61.jar:kotlin-stdlib-1.3.61.jar
$ kotlinc-jvm -cp $CP
The additional jar files are runtime dependencies of this JVM implmentation.
Getting Started with CFG in Kotlin / Java
+++++++++++++++++++++++++++++++++++++++++
.. include:: api-snip-01.inc
.. include:: kt-test0.cfg.txt
Loading a configuration
~~~~~~~~~~~~~~~~~~~~~~~
The configuration above can be loaded as shown below. In the REPL shell:
.. code-block:: kotlin
>>> import com.reddove.config.*
>>> val cfg = Config("path/to/test0.cfg")
Access elements with keys
~~~~~~~~~~~~~~~~~~~~~~~~~
Accessing elements of the configuration with a simple key is just like using a
``HashMap``:
.. code-block:: kotlin
>>> cfg["a"]
res3: kotlin.Any? = Hello,
>>> cfg["b"]
res4: kotlin.Any? = world!
Access elements with paths
~~~~~~~~~~~~~~~~~~~~~~~~~~
As well as simple keys, elements can also be accessed using :term:`path`
strings:
.. code-block:: kotlin
>>> cfg["c.d"]
res5: kotlin.Any = e
.. include:: api-snip-02.inc
.. code-block:: kotlin
>>> cfg["f.g"]
res6: kotlin.Any = h
.. include:: api-snip-03.inc
Access to date/time objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can also get native Java ``java.time.LocalDate`` and
``java.time.OffsetDateTime`` objects from a configuration, by using an ISO
date/time pattern in a :term:`backtick-string`:
.. code-block:: kotlin
>>> cfg["christmas_morning"]
res7: kotlin.Any = 2019-12-25T08:39:49
>>> cfg["christmas_morning"] as java.time.LocalDateTime
res8: java.time.LocalDateTime = 2019-12-25T08:39:49
The last statement shows that the returned object is actually an instance of
``java.time.LocalDateTime``.
Access to other JVM objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Access to other JVM objects is also possible using the
:term:`backtick-string` syntax, provided that they are one of:
* Environment variables
* Public static fields of public classes
* Public static methods without parameters of public classes
.. code-block:: kotlin
>>> import java.time.LocalDate
>>> cfg["today"] == LocalDate.now()
res9: kotlin.Boolean = true
>>> import java.lang.System
>>> cfg["output"] === System.out
res10: kotlin.Boolean = true
Accessing the "today" element of the above configuration invokes the static
method ``java.time.LocalDate.now()`` and returns its value.
Access to environment variables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To access an environment variable, use a :term:`backtick-string` of the form
```$VARNAME```:
.. code-block:: kotlin
>>> import java.lang.System
>>> cfg["home"] == System.getenv("HOME")
res11: kotlin.Boolean = true
.. include:: api-snip-07.inc
.. code-block:: kotlin
>>> cfg["foo"]
res12: kotlin.Any = bar
Access to computed values
~~~~~~~~~~~~~~~~~~~~~~~~~
.. include:: api-snip-04.inc
.. code-block:: cfg-body
:caption: test0a.cfg
total_period : 100
header_time: 0.3 * ${total_period}
steady_time: 0.5 * ${total_period}
trailer_time: 0.2 * ${total_period}
base_prefix: '/my/app/'
log_file: ${base_prefix} + 'test.log'
When this file is read in, the computed values can be accessed directly:
.. code-block:: kotlin
>>> import com.reddove.config.*
>>> val cfg = Config("test0a.cfg")
>>> cfg["header_time"]
res3: kotlin.Any = 30.0
>>> cfg["steady_time"]
res4: kotlin.Any = 50.0
>>> cfg["trailer_time"]
res5: kotlin.Any = 20.0
>>> cfg["log_file"]
res6: kotlin.Any = /my/app/test.log
Including one configuration inside another
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are times when it's useful to include one configuration inside another.
For example, consider the following configuration files:
.. code-block:: cfg-body
:caption: logging.cfg
layouts: {
brief: {
class: `org.apache.logging.log4j.core.layout.PatternLayout`,
pattern: '%d [%t] %p %c - %m%n'
}
},
appenders: {
file: {
level: 'INFO',
class: `org.apache.logging.log4j.core.appender.FileAppender`,
layout: 'brief',
filename: 'run/server.log',
append: false,
charset: 'UTF-8'
},
error: {
level: 'ERROR',
class: `org.apache.logging.log4j.core.appender.FileAppender`,
layout: 'brief',
filename: 'run/server-errors.log',
append: false,
charset: 'UTF-8'
},
debug: {
level: 'DEBUG',
class: `org.apache.logging.log4j.core.appender.FileAppender`,
layout: 'brief',
filename: 'run/server-debug.log',
append: false,
charset: 'UTF-8'
}
},
loggers: {
mylib: {
level: 'INFO'
}
'mylib.detail': {
level: 'DEBUG'
}
},
root: {
handlers: ['file', 'error', 'debug'],
level: 'WARNING'
}
.. code-block:: cfg-body
:caption: redirects.cfg
cookies: {
url: 'http://www.allaboutcookies.org/',
permanent: false
},
freeotp: {
url: 'https://freeotp.github.io/',
permanent: false
},
'google-auth': {
url: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2',
permanent: false
}
.. code-block:: cfg-body
:caption: main.cfg
secret: 'some random application secret',
port: 8000,
sitename: 'My Test Site',
default_access: 'public',
ignore_trailing_slashes: true,
site_options: {
want_ipinfo: false,
show_form: true,
cookie_bar: true
},
debug: true,
captcha_length: 4,
captcha_timeout: 5,
session_timeout: 7 * 24 * 60 * 60, # 7 days in seconds
redirects: @'redirects.cfg',
email: {
sender: 'no-reply@my-domain.com',
host: 'smtp.my-domain.com:587',
user: 'smtp-user',
password: 'smtp-pwd'
}
logging: @'logging.cfg'
.. include:: api-snip-05.inc
.. code-block:: kotlin
>>> import com.reddove.config.*
>>> val cfg = Config("main.cfg")
>>> cfg["logging.layouts.brief.class"]
res3: kotlin.Any = class org.apache.logging.log4j.core.layout.PatternLayout
>>> cfg["logging.appenders.file.class"]
res4: kotlin.Any = class org.apache.logging.log4j.core.appender.FileAppender
>>> cfg["redirects.freeotp.url"]
res5: kotlin.Any = https://freeotp.github.io/
>>> cfg["redirects.freeotp.permanent"]
res6: kotlin.Any = false
Avoiding unnecessary repetition
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. include:: api-snip-08.inc
.. code-block:: cfg-body
:caption: logging.cfg (partial)
appenders: {
file: {
level: 'INFO',
class: `org.apache.logging.log4j.core.appender.FileAppender`,
layout: 'brief',
filename: 'run/server.log',
append: false,
charset: 'UTF-8'
},
error: {
level: 'ERROR',
class: `org.apache.logging.log4j.core.appender.FileAppender`,
layout: 'brief',
filename: 'run/server-errors.log',
append: false,
charset: 'UTF-8'
},
debug: {
level: 'DEBUG',
class: `org.apache.logging.log4j.core.appender.FileAppender`,
layout: 'brief',
filename: 'run/server-debug.log',
append: false,
charset: 'UTF-8'
}
},
This portion could be rewritten as:
.. code-block:: cfg-body
:caption: logging.cfg (partial)
defs: {
base_appender: {
class: `org.apache.logging.log4j.core.appender.FileAppender`,
layout: 'brief',
append: false,
charset: 'UTF-8'
}
},
appenders: {
file: ${defs.base_appender} + {
level: 'INFO',
filename: 'run/server.log',
append: true,
},
error: ${defs.base_appender} + {
level: 'ERROR',
filename: 'run/server-errors.log',
},
debug: ${defs.base_appender} + {
level: 'DEBUG',
filename: 'run/server-debug.log',
}
},
.. include:: api-snip-06.inc
.. code-block:: kotlin
>>> import com.reddove.config.*
>>> val cfg = Config("main.cfg")
>>> cfg["logging.appenders.file.append"]
res3: kotlin.Any = true
>>> cfg["logging.appenders.file.filename"]
res4: kotlin.Any = run/server.log
>>> cfg["logging.appenders.file.level"]
res5: kotlin.Any = INFO
>>> cfg["logging.appenders.debug.level"]
res6: kotlin.Any = DEBUG
>>> cfg["logging.appenders.debug.filename"]
res7: kotlin.Any = run/server-debug.log
>>> cfg["logging.appenders.debug.append"]
res8: kotlin.Any = false
.. include:: api-snip-09.inc
.. code-block:: cfg-body
:caption: logging.cfg (partial)
defs: {
base_appender: {
class: `org.apache.logging.log4j.core.appender.FileAppender`,
layout: 'brief',
append: false,
charset: 'UTF-8'
}
log_prefix: 'run/',
},
appenders: {
file: ${defs.base_appender} + {
level: 'INFO',
filename: ${defs.log_prefix} + 'server.log',
append: true,
},
error: ${defs.base_appender} + {
level: 'ERROR',
filename: ${defs.log_prefix} + 'server-errors.log',
},
debug: ${defs.base_appender} + {
level: 'DEBUG',
filename: ${defs.log_prefix} + 'server-debug.log',
}
}
with the same result as before. It *is* slightly more verbose than before, but
the location of all files can be changed in just one place now, as opposed to
three, as it was before.
Classes
+++++++
The ``Config`` class
~~~~~~~~~~~~~~~~~~~~
This class implements a CFG configuration. You'll generally interface to CFG
files using this class. When you pass in a stream or file path to the
constructor or the :fun:`Config.load` / :fun:`Config.loadFile` methods, the
CFG source in the stream or file is parsed and converted into an internal form
that can be queried.
.. index::
single: Config; Kotlin class
.. class:: Config
.. cssclass:: class-members-heading
Variables
.. var:: noDuplicates: Boolean = true
Whether duplicates keys are allowed when parsing a mapping or mapping
body. If not allowed and duplicates are seen, a ``ConfigException`` is
thrown.
.. var:: strictConversions = true
If ``true``, a :class:`ConfigException` is thrown if a string can't be
converted. It's intended to help catch typos in backtick-strings.
.. var:: context: Map = hashMapOf()
A mapping within which to look up identifiers during evaluation of AST
nodes.
.. var:: cached: Boolean = false
If ``true``, an internal cache is used.
.. var:: includePath: ArrayList = arrayListOf()
A list of directories which is searched for included sub-configurations if the
parent directory of :var:`path` (or the current directory, if `path` isn't set)
doesn't contain them.
.. var:: path: String? = null
The path to the file from which the configuration has been read. You won't
usually need to set this, unless you want to load a configuration from text that
references included files. In that case, set the path to a value whose parent
directory contains the included files, or specify relevant directories using
:var:`includePath`. When a configuration is loaded from a file, this attribute
is automatically set.
.. cssclass:: class-members-heading
Constructors
.. constructor:: Config()
Constructs an instance with no actual configuration loaded. A call to
:fun:`load` or :fun:`loadFile` would be needed to actually make a
usable instance.
.. constructor:: Config(reader: java.io.Reader)
Construct this instance with the configuration read from the provided
reader.
.. constructor:: Config(path: String)
Construct this instance with the configuration read from a file named by
the provided path.
.. cssclass:: class-members-heading
Methods
.. fun:: load(reader : java.io.Reader)
Load this instance with the configuration read from the provided reader.
:param reader: A :class:`java.io.Reader` instance for the configuration
source.
:exception ConfigException: If there is an I/O error when reading the
source or an error in the configuration
syntax.
.. fun:: loadFile(path : String)
Load this instance with the configuration read from the file at the
provided path.
:param path: A String specifying the location of the file which contains
the configuration.
:exception ConfigException: If there is an I/O error when reading the
source or an error in the configuration
syntax.
.. fun:: get(key: String): Any
This method implements the key access operator, e.g. ``mapping[key]``.
The value of `key` can be a simple key (identifier) or a valid path
string.
:param key: A string describing a simple key or a path in the
configuration.
:throws InvalidPathException: If `key` is not present in the data and it
cannot be parsed as a path.
:throws BadIndexException: If, while traversing a path, a key or index is
not of the appropriate type for its container,
or if it isn't in the required range.
:throws ConfigException: If a key is not found or some other semantic
error occurs (for example, an operation
involving incompatible types).
.. fun:: get(key: String, defaultValue: Any): Any
This works similarly to the method above, except that if a key isn't
found, the ``defaultValue`` is returned.
:param key: A string describing a simple key or a path in the
configuration.
:param defaultValue: A value to return if the key or path is not
available in a configuration.
:throws InvalidPathException: If `key` is not present in the data and it
cannot be parsed as a path.
:throws BadIndexException: If, while traversing a path, a key or index is
not of the appropriate type for its container,
or if it isn't in the required range.
:throws ConfigException: If some other semantic error occurs (for
example, an operation involving incompatible
types).