owner
owner copied to clipboard
Q: how to support nested collections with unknown number of elements
Hi, this is a question related to nested collections or arrays of complex types. I saw already similar questions and answers, and also a possibility to use variable expansion, but I still didn't figure out how to deal with cases when amount of elements in collection is not known beforehand. Example: list of servers (each server has host, port, login), but amount of servers not known. How to represent it with properties or with XML supported by OWNER? server.uat.host=.. server.dev.host=... server.xxx.host=... and so on..
First I thought to have a property, which lists all server names: server.list=uat,dev,xxx,abc,foo And then OWNER will be able somehow to read and differentiate server.uat.host, server.dev.host, etc.., where uat,dev,etc are taken from server.list property. The main point here is that the list of server names is not know apriori and in every configuration file it can be different. But application should be able to get configuration for all specified servers. Is it possible to achieve with current version of OWNER?
Important aspect here is that we are talking about complex types, where there are many different attributes per server, so list "hostA:portA, hostB, hostC:portC" is not sufficient.
Here I post my solution to this problem. It is built using variable expansion and type conversion.
@Sources({"file:config.txt"})
public interface MyConfig extends Config {
@Separator(",")
@Key("servers.list")
@ConverterClass(ServerConverter.class)
List<Server> servers();
}
@Sources({"file:config.txt"})
public interface Server extends Config {
@Key("server.${server}.host")
String host();
@Key("server.${server}.port")
int port();
@Key("server.${server}.login")
String login();
}
public class ServerConverter implements Converter<Server> {
@Override
public Server convert(Method method, String input) {
Map<String, String> imports = new HashMap<String, String>();
imports.put("server", input);
Server server = ConfigFactory.create(Server.class, imports);
return server;
}
}
public class Main {
public static void main(String... args) {
MyConfig config = ConfigFactory.create(MyConfig.class);
List<Server> servers = config.servers();
for (Server s : servers) {
System.out.println("Host: "+s.host());
System.out.println("Port: "+s.port());
System.out.println("Login: "+s.login());
}
}
}
And then you will be able to define a configuration with nested structure. Similarly, Server object may have other lists of objects and so on.. You will have to call ConfigFactory.create every time you read a list of objects but I haven't found any better way to deal with such configurations. Supported configuration for this example can look like following:
servers.list=serverA,serverB
server.serverA.host=10.10.10.11
server.serverA.port=1025
server.serverA.login=alice
server.serverB.host=10.10.10.12
server.serverB.port=1026
server.serverB.login=bob
Quite brilliant I must say. :+1:
In above example:
When the configuration file is maintained manually and there are a lot of servers, it is quite easy to forget to add/remove the sever name in the servers.list
. Below is an example that removes the server names duplication. Slightly more code, but worth it IMHO.
Use an Accessible
configuration and parse the property names for the available server names. The configuration file then can look like this.
server.serverA.host=10.10.10.11
server.serverA.port=1025
server.serverA.login=alice
server.serverB.host=10.10.10.12
server.serverB.port=1026
server.serverB.login=bob
Working example:
package owner.example;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.aeonbits.owner.Accessible;
import org.aeonbits.owner.Config;
import org.aeonbits.owner.Config.Sources;
import org.aeonbits.owner.ConfigFactory;
import org.aeonbits.owner.Converter;
public class Main {
static final String CONFIG_FILE = "classpath:owner/example/config.txt";
@Sources({ CONFIG_FILE })
public interface MyConfig extends Config {
@Separator(",")
@Key("servers.list")
@ConverterClass(ServerConverter.class)
@DefaultValue("${server.names}") // <-- note default value
List<Server> servers();
}
@Sources({ CONFIG_FILE })
public interface Server extends Config {
@Key("server.${server}.host")
String host();
@Key("server.${server}.port")
int port();
@Key("server.${server}.login")
String login();
}
public static class ServerConverter implements Converter<Server> {
@Override
public Server convert(Method method, String input) {
Map<String, String> imports = new HashMap<String, String>();
imports.put("server", input);
Server server = ConfigFactory.create(Server.class, imports);
return server;
}
}
public static void main(String[] args) {
MyConfig config = myConfig();
List<Server> servers = config.servers();
for (Server s : servers) {
System.out.println("Host: " + s.host());
System.out.println("Port: " + s.port());
System.out.println("Login: " + s.login());
}
}
static MyConfig myConfig() {
String serverNames = parseServerNames();
Map<String, String> imports = new HashMap<String, String>();
imports.put("server.names", serverNames);
return ConfigFactory.create(MyConfig.class, imports);
}
@Sources({ CONFIG_FILE })
private interface ServerNames extends Accessible {
@Key("server.name.pattern")
@DefaultValue("server\\.(.*)\\.host")
String serverNamePattern();
}
private static String parseServerNames() {
ServerNames cfg = ConfigFactory.create(ServerNames.class);
Pattern p = Pattern.compile(cfg.serverNamePattern());
// use a set to avoid duplicates
Set<String> serverNames = new LinkedHashSet<String>();
for (String propertyName : cfg.propertyNames()) {
Matcher m = p.matcher(propertyName);
if (m.matches()) {
serverNames.add(m.group(1));
}
}
return serverNames.toString().replaceAll("^\\[", "").replaceAll("\\]$", "");
}
}