Config is an agnostic key-based configuration library, built on top of the concept of holding reference to a value in the configuration instead of holding the value itself.
Map oriented
The library is totally map-oriented, it stores and reads values from Map
s, also even if there is a concept of Storage
classes, which is the medium of the data storage, and they could store data in any format, most of the common implementations uses Maps and all keys are String-only, this is intentional, and we focus only in storing data in Map
s.
All values are serialized upfront before being stored, in other words, when they are sent to a Storage
medium, they are already serialized using built-in and custom serializers. Any library capable of rendering Map
s could take advantage of Config
.
Key based/Pointer oriented
Config uses a pointer-oriented data access and write, this allows data changes to be reflected all across the application, and config-reloading could be simply implemented by using File Watchers.
Agnostic
Config does not care about the final format of the configuration file, it only manages data in a Map
. The class responsible for loading and saving this Map
in the configuration is the Backend
class. It also provides which types are supported to be stored in the Map
without the serialization and deserialization process.
We provide common backend implementations for most used configuration languages, such as Json, Yaml, Toml and XML. Since all of those libraries are able to render Maps
as Strings, they become very handful for application configuration.
Serialization supported
Serialization of custom types are supported through Serializers
registry and Serializer
implementation, and it is very easy to implement your own serializer. Also, API provides interfaces for implementing serialization of complex types.
Type information retention
Config depends on types provided by TypeInfo
tokens for implementing Serialization for Map
, List
and user defined types.
This allows generic types to be serialized correctly and using the right serializer, instead of relying on the type of the object itself. This means that types must be explicitly provided, and the information must be retained at runtime.
Getting Started
Config and official backends are available through jitpack.io.
Gradle (Kotlin)
val config_version by project
repositories {
mavenCentral()
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.JonathanxD.Config:Config:$config_version")
implementation("com.github.JonathanxD.Config:Config-Toml:$config_version") // Toml backend
}
Reading and writing values
Given the following Toml config:
[server]
addr = "0.0.0.0"
port = 8080
You could write something like this to read and write values:
public class ServerConfig {
private final Config config;
private final Key<String> addr;
private final Key<Long> port;
public ServerConfig(Path configPath) {
ConfigIO io = ConfigIO.path(configPath, StandardCharsets.UTF_8);
Backend backend = new TomlBackend(io);
this.config = new Config(backend);
this.config.load(); // Config file must exists
Key<Void> serverSection = this.config.getRootKey().getKeySection("server");
this.addr = serverSection.getKey("addr", CommonTypes.STRING);
this.port = serverSection.getKey("port", CommonTypes.LONG);
}
public String getAddress() {
return this.addr.getValue();
}
public long getPort() {
return this.port.getValue();
}
public void setPort(long port) {
this.port.setValue(port);
}
public void save() {
this.config.save();
}
}
Serialization
Config already implements serializers for basic types and date types, and some JwIUtils library types, such as Text
and TypeInfo
(you could find all here), however, sometimes you want to work with your custom types (or 3rd party types) which does not have default serializers implemented, for this, Config provides Serializers
class as registry base for Serializer
implementations.
Basic Serializer
record User(String name, String email) {
}
public class UserSerializer implements Serializer<User> {
@Override
public void serialize(User value,
Key<User> key,
TypeInfo<?> typeInfo,
Storage storage,
Serializers serializers) {
key.getKey("name", String.class).setValue(value.name());
key.getKey("email", String.class).setValue(value.email());
}
@Override
public User deserialize(Key<User> key,
TypeInfo<?> typeInfo,
Storage storage,
Serializers serializers) {
String name = key.getKey("name", String.class).getValue();
String email = key.getKey("email", String.class).getValue();
return new User(name, email);
}
}
Also, sometimes you just want to store a single value, for this you could use Key.getAs
to transform the Key type:
record User(String name) {
}
public class UserSerializer implements Serializer<User> {
@Override
public void serialize(User value,
Key<User> key,
TypeInfo<?> typeInfo,
Storage storage,
Serializers serializers) {
key.getAs(String.class).setValue(value.name());
}
@Override
public User deserialize(Key<User> key,
TypeInfo<?> typeInfo,
Storage storage,
Serializers serializers) {
return new User(key.getAs(String.class).getValue());
}
}
Calling other serializers
Config automatically invoke other serializers to proceed with the serialization when a value is not supported by the backend, so the code below will work correctly and serialize the LocalDate
as well.
record User(LocalDate registrationDate, String name) {
}
public class UserSerializer implements Serializer<User> {
@Override
public void serialize(User value,
Key<User> key,
TypeInfo<?> typeInfo,
Storage storage,
Serializers serializers) {
key.getKey("registrationDate", LocalDate.class).setValue(value.email());
key.getKey("name", String.class).setValue(value.name());
}
@Override
public User deserialize(Key<User> key,
TypeInfo<?> typeInfo,
Storage storage,
Serializers serializers) {
LocalDate registrationDate = key.getKey("registrationDate", LocalDate.class).getValue();
String name = key.getKey("name", String.class).getValue();
return new User(registrationDate, name);
}
}
However, when working with complex types which need an intermediate storage, this is not enough. For these cases, you could use the Serializers
provided to serialize
and deserialize
methods. See below a hardcore version of serialize
which does the same thing as the version above, however using intermediate storage:
record User(LocalDate registrationDate, String name) {
}
public class UserSerializer implements Serializer<User> {
@Override
public void serialize(User value, Key<User> key, TypeInfo<?> typeInfo, Storage storage, Serializers serializers) {
Map<Object, Object> newMap = new LinkedHashMap<>();
Map<String, Object> temp = new LinkedHashMap<>();
Storage newStorage = Storage.createMapStorage(key, temp);
{
Key<LocalDate> regDateKey = key.getAs("registrationDate", LocalDate.class, newStorage);
Object date = serializers.serializeUncheckedAndGet(value.getRegistrationDate(), regDateKey);
temp.clear();
newMap.put("registrationDate", date);
}
{
Key<String> nameKey = key.getAs("name", String.class, newStorage);
Object name = serializers.serializeUncheckedAndGet(value.getName(), nameKey);
temp.clear();
newMap.put("name", name);
}
storage.pushValue(key, newMap);
}
@Override
public User deserialize(Key<User> key,
TypeInfo<?> typeInfo,
Storage storage,
Serializers serializers) {
LocalDate registrationDate = key.getKey("registrationDate", LocalDate.class).getValue();
String name = key.getKey("name", String.class).getValue();
return new User(registrationDate, name);
}
}
This approach is used by ListSerializer
and MapSerializer
to correctly serialize values in a controlled Storage
medium. When you call serializers.serialize*
with this intermediate storage, instead of serializing the value inside the Storage
provided to serialize
method, it does by serializing the value inside the provided Storage
medium, thus serializing the value inside a controlled context, which will not overwrite or modify the values already stored.
With this, introspecting the values and changing the structure of them is more safe, as it will not damage the main storage medium (commonly the Config
storage), and at in the end of serialization process, you can push to the main storage only the values you care about.
Backend
class
Config itself does not implement any configuration format, instead, it backs the configuration loading and saving logic to a backend.
Currently, the following backend implementations are officially supported:
- Config-Jackson (Using fasterxml.jackson)
- Config-XML (Using fasterxml.jackson-xml)
- Config-Json (Using JSON.simple)
- Config-Yaml (Using snakeyaml YAML 1.1 compliant)
- Config-Yaml-1.2 (Using snakeyaml-engine YAML 1.2 compliant)
- Config-Toml (Using tomlj 1.0.0-rc.1 compliant. With an experimental renderer, as of the time the implementation was written, there is no official TOML writer for Java).
You could choose one of these backends to load and save your configuration. Also, you are free to write your own Backend implementation, as they are very simple. Config works solely with Java Map
, List
, String
and primitive types.
Also, Config is not a real-time configuration editor, it will not save the configuration for every update that occurs in the Config object, you need to manually save and load using Config.save
and Config.load
. If you need a real-time configuration editor with reload capabilities, you could easily write a File Watcher or Scheduler to reload configuration using those methods.
Jackson Backend
Writes and reads json using Jackson ObjectMapper.
Jackson-XML Backend
Writes and reads XML using Jackson XmlMapper.
Json backend
Writes and reads json using JSON.simple.
Yaml Backend
Writes and reads Yaml 1.1 using snakeyaml.
Toml Backend
Reads TOML compliant to 1.0.0-rc.1 using tomlj and writes TOML using a custom implementation of TOML rendering.
Note
The Toml backend is able to read any TOML configuration file compliant to 1.0.0-rc.1, however, for writing Toml config, it uses an experimental renderer as there is no recent and maintained Toml library for Java which allows Toml rendering.
This means that Toml files saved by TOML backend ends up being different from the original, and is not rendered in the best way it could be following the Toml standards.
YAML 1.2 Backend
Reads and writes Yaml 1.2 using snakeyaml-engine.
Note
There is a Yaml 1.2 backend which uses snakeyaml-engine to read and write yaml files, however, since Config
is fully map-oriented, yaml files which does not have any keys, like this one:
- First
- Second
- Third
Are loaded normally, but an intermediate section is created, named .
. This section allows loading those values normally, and rendering them is made through a special logic which keeps this structure (as long as there is no new keys defined).
Also, Config
calls the Backend
to resolve the root key, then the Yaml backend resolve the .
as default key, thus allowing to load those values seamlessly without careing about the intermediate section:
public class ConfigLoader {
public static Config loadYaml() {
// Yaml loading logic...
Config config = new Config(new YamlBackend(...));
TypeInfo<List<String>> stringListTypeInfo = TypeInfo.builderOf(List.class).of(String.class).buildGeneric();
Key<List<String>> values = config.getRootKey().getAs(stringListTypeInfo);
}
}
It is important to know this, because creating a configuration with a key named .
with a List value will trigger this behavior. But only for Yaml 1.2 backend.
Maps inside the List
When there is a map inside the list, like this:
- First
- Second
- Third
- Somedata: value
Config will be able to handle this situation, but with limited capabilities. It is able to resolve the somedata
key and change its value:
public class ConfigLoader {
public static Config loadYaml() {
// Yaml loading logic...
Config config = new Config(new YamlBackend(...));
Key<List<Object>> values = config.getRootKey().getAs(CommonTypes.LIST_OF_OBJECT);
Key<String> somedata = values.getKey("somedata", String.class);
somedata.setValue("newValue");
}
}
It is able to create new maps inside the list when needed:
public class ConfigLoader {
public static Config loadYaml() {
// Yaml loading logic...
Config config = new Config(new YamlBackend(...));
Key<List<Object>> values = config.getRootKey().getAs(CommonTypes.LIST_OF_OBJECT);
Key<String> somedata = values.getKey("somedata2", String.class); // new map is created to handle this key
somedata.setValue("newValue2");
}
}
And access and change values in a specific index of a list (which must exists):
public class ConfigLoader {
public static Config loadYaml() {
// Yaml loading logic...
Config config = new Config(new YamlBackend(...));
Key<List<Object>> values = config.getRootKey().getAs(CommonTypes.LIST_OF_OBJECT);
Key<String> index0 = IndexKey.forKeyAndIndex(values, 0);
index0.setValue("newValue2");
}
}
However, not all features that Yaml 1.2 supports were tested.
Config
class
Config is a fully map-based Storage
implementation, it uses a LinkedHashMap
as storage medium, while checks for supported value types using Backend.supports
. Thus, every value stored in this class is directly stored in the wrapped LinkedHashMap
, and when configuration need to be saved to file, it just sends an unmodifiable copy of this map
to the backend
(it must be a copy and unmodifiable to avoid concurrency issues).
Also, Config is not Thread-safe by default, this means that to concurrently write values, you need to implement a locking logic to allow only one modification at a time. We plan to provide a concurrent capable implementation in the future.
Key
The most important class of the Config library. A Key represents a pointer to a value in the configuration file, tied to a key-path. With this pointer you are able to read and write values directly to the Storage
medium without even needing to hold a reference to the Storage
itself. Also, you could have different pointers to the same value, and changing a value through one pointer will be reflected in all other pointers (they are not really reflected, they just read the value from the same path, so if the value changes, anyone pointing to this path has access to the new value).
KeySpec
Is a specification of a Key
name and type, with KeySpec
you refer to a part of the key path. It is used to implement a more readable Key
access with constant path parts.
Serializers
Config also supports serializers. This is very important for the library as it only works with Maps
, Lists
, Strings
and primitive types (different backends could provide support for others types, but the official ones does not).
Serializers are registered and provided through Serializers
class, custom serializers must implement Serializer
interface and be registered in Config.getSerializers
.