.. _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).