Codes are a puzzle


Creating Custom Configuration in Quarkus Loaded from JSON File

Introduction

Quarkus, a framework for building lightweight, fast, and efficient Java applications, offers developers the flexibility to create custom configurations loaded from JSON files. These custom configurations can be seamlessly integrated into your Quarkus application, enhancing its configurability and adaptability. To achieve this, Quarkus utilizes Eclipse MicroProfile Config ( MP-Config), with the SmallRye implementation providing the necessary tools. In this article, we’ll delve into the process of crafting custom configurations and loading them from JSON files within Quarkus, all while exploring the mechanics of SmallRye’s MP-Config implementation. Additionally, we’ll showcase the creation and registration of a custom ConfigSource and ConfigSourceFactory, which play a pivotal role in this configuration management approach.

Understanding MicroProfile Config in Quarkus

Quarkus internally relies on the SmallRye implementation of MP-Config. This implementation allows developers to incorporate Configuration Sources, which provide configuration data from various origins. These sources can be files with non-standard formats, or even data retrieved from a central repository. MP-Config ensures a deterministic ordering of configuration sources based on their ordinal values when multiple sources contain the same configuration key.

SmallRye’s implementation of MP-Config facilitates the creation of new ConfigSources and ConfigSourceFactories. The ConfigSourceFactory has knowledge of all previously defined sources, enabling developers to read those values and pass them to a newly created ConfigSource. The registration of these custom sources and factories is accomplished through the Java ServiceLoader interface, with a specific file called io.smallrye.config.ConfigSourceFactory being placed in the META-INF/services/ directory. This file provides the fully qualified names of the custom sources and factories.

Implementing a custom JSON Configuration

To demonstrate the creation and registration of a custom ConfigSource and ConfigSourceFactory in Quarkus, we’ll focus on a practical example - an JsonConfigSource and JsonConfigSourceFactory pair. This custom configuration source allows you to read external JSON configuration files and integrate them into your Quarkus application.

The JsonConfigSource class is responsible for reading and providing configuration properties from a JSON file. It implements the ConfigSource interface and overrides several methods to interact with the Quarkus configuration system.

Here is an overview of its key functionalities:

  • Reading a JSON file and parsing it into a JsonObject.
  • Providing configuration properties, including handling default values.
  • Specifying the source’s ordinal value.
  • Assigning a unique name to the source, which will be used for registration.
 1
 2@Slf4j
 3public class JsonConfigSource implements ConfigSource {
 4
 5  private final Map<String, ConfigValue> existingValues;
 6
 7  private JsonObject root;
 8
 9  public JsonConfigSource(final Map<String, ConfigValue> exProp) {
10    existingValues = exProp;
11  }
12
13  public void addJsonConfigurations(final ConfigValue config) {
14    final File file = new File(config.getValue());
15
16    if (!file.canRead()) {
17      log.warn("Can't read config from " + file.getAbsolutePath() + "");
18    } else {
19      try (final InputStream fis = new FileInputStream(file);
20          final JsonReader reader = Json.createReader(fis)) {
21
22        root = reader.readObject();
23      } catch (final IOException ioe) {
24        log.warn("Reading the config failed: " + ioe.getMessage());
25      }
26    }
27  }
28
29  @Override
30  public Map<String, String> getProperties() {
31
32    final Map<String, String> props = new HashMap<>();
33    final Set<Map.Entry<String, ConfigValue>> entries = existingValues.entrySet();
34    for (final Map.Entry<String, ConfigValue> entry : entries) {
35      String newVal = getValue(entry.getKey());
36      if (newVal == null) {
37        newVal = entry.getValue().getValue();
38      }
39      props.put(entry.getKey(), newVal);
40    }
41
42    return props;
43  }
44
45  @Override
46  public Set<String> getPropertyNames() {
47    return existingValues.keySet();
48  }
49
50  @Override
51  public int getOrdinal() {
52    return 270;
53  }
54
55  @Override
56  public String getValue(final String configKey) {
57
58    final JsonValue jsonValue = root.get(configKey);
59
60    if (jsonValue != null) {
61      return getStringValue(jsonValue);
62    }
63
64    if (existingValues.containsKey(configKey)) {
65      return existingValues.get(configKey).getValue();
66    } else {
67      return null;
68    }
69  }
70
71  @Override
72  public String getName() {
73    return "EXTERNAL_JSON";
74  }
75
76  private String getStringValue(final JsonValue jsonValue) {
77    if (jsonValue != null) {
78      final JsonValue.ValueType valueType = jsonValue.getValueType();
79
80      if (valueType == JsonValue.ValueType.STRING) {
81        return ((JsonString) jsonValue).getString();
82      } else if (valueType == JsonValue.ValueType.NUMBER) {
83        // Handle integer and floating-point numbers
84        return jsonValue.toString();
85      } else if (valueType == JsonValue.ValueType.TRUE || valueType == JsonValue.ValueType.FALSE) {
86        // Handle boolean values
87        return Boolean.toString(jsonValue.getValueType() == JsonValue.ValueType.TRUE);
88      } else if (valueType == JsonValue.ValueType.NULL) {
89        // Handle null values
90        return null;
91      }
92    }
93    return null;
94  }
95}

The JsonConfigSourceFactory class is a custom ConfigSourceFactory responsible for creating and configuring instances of the JsonConfigSource. It also defines a unique priority for this factory.

Here is an overview of its key functionalities:

  • Retrieving the path of the JSON configuration file from the Quarkus configuration.
  • Building an instance of JsonConfigSource.
  • Assigning a priority value to the factory.
 1
 2@Slf4j
 3public class JsonConfigSourceFactory implements ConfigSourceFactory {
 4
 5  public static final String CONFIG_JSON_FILE = "config.json.file";
 6
 7  @Override
 8  public Iterable<ConfigSource> getConfigSources(final ConfigSourceContext configSourceContext) {
 9    final ConfigValue value = configSourceContext.getValue(CONFIG_JSON_FILE);
10
11    if (value == null || value.getValue() == null) {
12      return Collections.emptyList();
13    }
14
15    final Map<String, ConfigValue> exProp = new HashMap<>();
16    final Iterator<String> stringIterator = configSourceContext.iterateNames();
17
18    while (stringIterator.hasNext()) {
19      final String key = stringIterator.next();
20      final ConfigValue cValue = configSourceContext.getValue(key);
21      exProp.put(key, cValue);
22    }
23
24    final JsonConfigSource configSource = new JsonConfigSource(exProp);
25    final List<ConfigValue> configValueList = List.of(value);
26
27    for (final ConfigValue config : configValueList) {
28      if (ConfigExists(config)) {
29        configSource.addJsonConfigurations(config);
30      }
31    }
32
33    return Collections.singletonList(configSource);
34  }
35
36  @Override
37  public OptionalInt getPriority() {
38    return OptionalInt.of(270);
39  }
40
41  private boolean ConfigExists(final ConfigValue config) {
42
43    if (config == null || config.getValue() == null) {
44      log.warn("The given ConfigValue object is null");
45      return false;
46    } else if (!(Files.exists(Path.of(config.getValue())))) {
47      return false;
48    }
49
50    return true;
51  }
52}

Registration via ServiceLoader

Both the JsonConfigSource and JsonConfigSourceFactory are registered with the Quarkus application through the Java ServiceLoader mechanism. A file named io.smallrye.config.ConfigSourceFactory is placed in the META-INF/services/ directory. This file contains the fully qualified name of the JsonConfigSourceFactory, enabling Quarkus to discover and use it.

The JSON file

1{
2  "simple.service": "pusher",
3  "simple.source": "source",
4  "simple.destination": "destination"
5}

Define Your Configuration Interface

 1
 2@ConfigMapping(prefix = "simple")
 3public interface SimpleConfig {
 4
 5  @WithName("source")
 6  String source();
 7
 8  @WithName("service")
 9  String service();
10
11  @WithName("destination")
12  String destination();
13}

Conclusion

Eclipse MicroProfile Config, in conjunction with the SmallRye implementation, empowers Quarkus developers to manage their application’s configuration efficiently. The ability to create and register custom ConfigSources and ConfigSourceFactories, as demonstrated with the JsonConfigSource and JsonConfigSourceFactory, extends the flexibility and utility of the framework. By following these guidelines, you can seamlessly integrate external configuration data, such as JSON files, into your Quarkus application, enhancing its configurability and adaptability.

By understanding these concepts and leveraging the power of MicroProfile Config, you can further optimize your Quarkus application’s configuration management and streamline the development process.

The full source code is available at Github