Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
bodunov opened this issue Jun 10, 2015 · 3 comments
Open

Comments

@bodunov
Copy link

bodunov commented Jun 10, 2015

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 bodunov changed the title Q: how to support an array with unknown number of elements Q: how to support nested collections with unknown number of elements Jun 10, 2015
@bodunov
Copy link
Author

bodunov commented Jun 29, 2015

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

@lviggiano
Copy link
Collaborator

Quite brilliant I must say. 👍

@drapostolos
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants