Maps in Lists and Lists in Maps in Groovy

I'm trying to make friends with Groovy because Jenkins uses Groovy and I need to be friends with Jenkins. Groovy is a "Java-syntax-compatible" language that runs on the JVM. Not entirely reassuring to see that Wikipedia says its "Typing discipline" is "Dynamic, static, strong, duck," which is a little like saying "north, south, black, white" - two sets of opposite things.

Because of inconsistent naming schemes, I have to deploy to servers that have nothing in the same place: some server sets have one server, some have two. And on those servers, one or two instances may be running. In a small blessing, the deploy folders have the same name on servers in the same environment (although they differ between environments). So we end up with a very complex variable to describe the servers:

def serverSets = [
    "qa": [
        "physicalServers" : [
            'loki': ['loki'],
        ],
        "deployFolder": '/home/loki/htdocs/',
        "gitBranch"   : 'qa',
    ],
    "dev": [
        "physicalServers" : [
            'thor': ['asgard'],
        ],
        "deployFolder": '/var/www/dev/',
        "gitBranch"   : 'dev',
    ],
    "prod" : [
        "physicalServers" : [
            'frigg': ['freya', 'frey'],
            'odin' : ['odin3', 'odin4'],
        ],
        "deployFolder": '/var/www/',
        "gitBranch"   : 'master',
    ],
]

That's right: a list inside a map inside a map inside a map. That can't possibly go wrong. Sadly, it's the simplest data setup I could come up with ...

Better tutorials than I could write have been written about iterating over lists and maps (what I think of as "dictionaries" because I'm more familiar with Python, but are most formally known as an Associative Array): I recommend Working with collections from Groovy's own documentation. But what it doesn't tell you is how to deal with those nested complex variables/collections.

The first time I experimented with <list-or-map>.each {} closures, I swear that the ${it} variable didn't behave properly - at the second level it returned its first level value. And now I can't replicate the problem, as this works fine:

def listmap = [
    [a: 4, b: 16, c: 64],
    [x: 5, y: 25, z: 625],
]

println('Using "it":');
listmap.each {
    def newmap = it;
    println("first level item: " + newmap);
    newmap.each {
        println("    ${it.key}: ${it.value}");
    }
}

But even if you didn't need to name the iterator variable to save yourself from that problem, named closure variables make the process both simpler and clearer:

def listmap = [
    [a: 4, b: 16, c: 64],
    [x: 5, y: 25, z: 625],
]

println('Using specific names:');
listmap.each { first ->
    println("first level item: " + first);
    first.each { second ->
        println("    ${second.key} ... ${second.value}");
    }
}

So how to deal with that very complex variable from the beginning of the post?

def serverSets = [
    "qa": [
        "physicalServers" : [
            'loki': ['loki'],
        ],
        "deployFolder": '/home/loki/htdocs/',
        "gitBranch"   : 'qa',
    ],
    "dev": [
        "physicalServers" : [
            'thor': ['asgard'],
        ],
        "deployFolder": '/var/www/dev/',
        "gitBranch"   : 'dev',
    ],
    "prod" : [
        "physicalServers" : [
            'frigg': ['freya', 'frey'],
            'odin' : ['odin3', 'odin4'],
        ],
        "deployFolder": '/var/www/',
        "gitBranch"   : 'master',
    ],
]

['qa', 'dev', 'prod'].each { env ->
    def serverSet = serverSets[env];
    print('\nServer set is "' + env + '"');
    println(' with deploy folder ' + serverSet["deployFolder"]);
    def numServers = serverSet['physicalServers'].size();
    println("env ${env} has ${numServers} physical servers:");
    serverSet['physicalServers'].each { phyServ ->
        println("    " + phyServ.key + ":");
        phyServ.value.collect { softServ ->
            println("        " + softServ);
        }
    }
}

The output looks like this:

Server set is "qa" with deploy folder /home/loki/htdocs/
env qa has 1 physical servers:
    loki:
        loki

Server set is "dev" with deploy folder /var/www/dev/
env dev has 1 physical servers:
    thor:
        asgard

Server set is "prod" with deploy folder /var/www/
env prod has 2 physical servers:
    frigg:
        freya
        frey
    odin:
        odin3
        odin4

The code takes a minute to parse out but in the end is more manageable than I expected, and all parts of the complex variable are fairly easily accessible.

Bibliography