Java - SnakeYaml | Get all keys of a file

Viewed 602

First of all, I have been reading a few posts about keys, but none of them asks my question on how to get ALL keys of a yaml file, only on how to get an specific key.

Now, I want to create a file updater, it works, but it only updates the first keys, without the "sub-keys", here is the code:

InputStream resource = getClass().getClassLoader().getResourceAsStream(dir);
Map<String, Object> data = new Yaml().load(resource);
for(String str : data.keySet()) {
    DBot.getConsole().log(str);
    if(!contains(str)) {
        set(str, data.get(str));
    }
}

The file looks like this:

Features.Example.StringA

Features.Example.StringB

With points being spaces to make them sub-keys (stack overflow puts them on a single line, sorry)

Now the thing is, the updater will only work if "Features" is deleted, also, the debug will only print "Features", meaning that only the first key is on the key set, how can I get all keys?

2 Answers

I have finally found how to return a Set with every key separated by a ".", Bukkit/Spigot developers might be familiar with this. First of all, you have to create a class like this:

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class YamlKeys {

    private static Set<String> keys = new HashSet<String>();
    private static String path = "";

    YamlKeys(Map<?, ?> data) {
        getKeysRecursive(data);
    }

    private void getKeysRecursive(final Map<?, ?> data) {
        for(Object key : data.keySet()) {
            final Object value = data.get(key);
            if(key instanceof String) {
                if(path.length() == 0) {
                    path = (String)key; // If the key is the first on the path, don't include separator.
                } else {
                    path = path+"."+(String)key; // Here is the separator, you can change it.
                }
            }
            if(value instanceof Map) {
                getKeysRecursive((Map<?, ?>) value); // A value map has been found, recursing with that value.
            } else {
                keys.add(path); // No more maps have been found, we can add the path and stop recursing.
                if(path.contains(".")) {
                    path = path.substring(0, path.lastIndexOf(".")); // Removing last key, so if a value contains more than one key, it won't appear again.
                }
            }
        }
        path = ""; // This is important, reset the path.
    }

    Set<String> getKeys() {
        return keys;
    }
}

Then, to call it and select if you want to get deep keys or "normal" keys, you can create a method like this:

public Set<String> getKeys(boolean deep) {
    Map<String, Object> data = new Yaml().load(inStream);
    if(!deep) {
        return data.keySet();
    } else {
        return new YamlKeys(data).getKeys();
    }
}

To test it, we can use the following code:

new YamlKeys(data).getKeys().stream().forEach(key -> System.out.println(key));

With this file:

FirstKey:
  SecondKey:
    Enabled: true
    Text: "Some text"
    AnotherKey:
      AValue: true
AnotherTest:
  Enabled: false
Value: true

It returns this output:

FirstKey.SecondKey.AnotherKey.AValue
FirstKey.SecondKey.Enabled
FirstKey.SecondKey.Text
Value
AnotherTest.Enabled

Thanks to roby for telling me about recursion.

SnakeYAML is decoding the yaml into a recursive data structure. For example:

public static void main(String[] args) {
    String yaml = "a:\n  b: \n    c: \"string\"";
    Map<String, Object> data = new Yaml().load(yaml);
    System.out.println(data);
}

prints out:

{a={b={c=string}}}

Which is a Map<String, Map<String, Map<String, String>>>.

To show how you can work with it recursively, here's how you can print out some of that detail.

private static void printMapRecursive(final Map<?, ?> data) {
        for(Object key : data.keySet()) {
            System.out.println("key " + key + " is type " + key.getClass().getSimpleName());
            final Object value = data.get(key);
            if(value instanceof Map){
                System.out.println("value for " + key + " is a Map - recursing");
                printMapRecursive((Map<?, ?>) value);
            } else {
                System.out.println("value " + value + " for " + key + " is type " + value.getClass());
            }
        }
    }

Which you can call with printMapRecursive(data); and see output:

key a is type String
value for a is a Map - recursing
key b is type String
value for b is a Map - recursing
key c is type String
value string for c is type class java.lang.String

and an example of recursively transforming the keys:

private static Map<?, ?> mutateMapRecursive(final Map<?, ?> data,
                                            Function<String, String> keyFunction) {
        Map<Object, Object> result = new HashMap<>();
        for (Object key : data.keySet()) {
            final Object value = data.get(key);
            if(key instanceof String){
                key = keyFunction.apply((String) key);
            }
            if (value instanceof Map) {
                result.put(key, mutateMapRecursive((Map<?, ?>) value, keyFunction));
            }
            else {
                result.put(key, value);
            }
        }
        return result;
    }

called like:

final Map<?, ?> transformed = mutateMapRecursive(data, (key) -> "prefix_" + key);
System.out.println(transformed);

emits:

{prefix_a={prefix_b={prefix_c=string}}}

Related