owner icon indicating copy to clipboard operation
owner copied to clipboard

Q: how to support nested collections with unknown number of elements

Open bodunov opened this issue 9 years ago • 3 comments

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.

bodunov avatar Jun 10 '15 11:06 bodunov

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

bodunov avatar Jun 29 '15 11:06 bodunov

Quite brilliant I must say. :+1:

lviggiano avatar Jun 29 '15 14:06 lviggiano

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("\\]$", "");
    }
}

drapostolos avatar Jul 20 '15 22:07 drapostolos