hhaslam11 hhaslam11 - 3 months ago 27
YAML Question

Java not saving all data

I have a program that saves/loads data to a text file. However, some data seems to not be saving, and I can't figure out why.

Here's a simplified version of the code.

import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.representer.Representer;

import java.io.*;
import java.nio.charset.Charset;
import java.util.*;

public class Test {

private static DumperOptions options;
private static File saveLocation;
private static Map<String, Object> data;
private static Representer representer;
private static String path;
private static Yaml yaml;

private static void setup() {
saveLocation = new File(path);

if (!saveLocation.exists())
try {
saveLocation.createNewFile();
} catch (IOException exception) {
exception.printStackTrace();
}

setupDumper();

yaml = new Yaml(representer, options);
data = Collections.synchronizedMap(new LinkedHashMap<String, Object>());
}

private static void setupDumper() {
options = new DumperOptions();
representer = new Representer();

options.setIndent(2);
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setAllowUnicode(Charset.defaultCharset().name().contains("UTF"));
representer.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
}

public static boolean contains(String key) {
return data.containsKey(key);
}

public static boolean exists() {
return data != null;
}

public static boolean hasValue(String key) {
Object tempObject = data.get(key);

return (tempObject != null);
}

public static boolean isEmpty() {
return data.isEmpty() || data == null;
}

public static int getInteger(String key) {
Object tempObject = get(key);

if (tempObject instanceof Integer)
return (Integer) tempObject;
if (tempObject instanceof String)
return Integer.parseInt(tempObject.toString());
if (tempObject instanceof Number)
return Integer.parseInt(tempObject.toString());
return -1;
}

public static Object get(String key) {
if (isEmpty())
return null;

if (key.contains(".")) {
String[] nodes = key.split("\\.");
Map<String, Object> currParent;

if (data.containsKey(nodes[0]) && (data.get(nodes[0]) instanceof Map))
currParent = (Map) data.get(nodes[0]);
else return null;

if (nodes.length > 1) {
for (int i = 1; i < nodes.length - 1; i++) {
if (currParent.containsKey(nodes[i]) && (currParent.get(nodes[i]) instanceof Map))
currParent = (Map) currParent.get(nodes[i]);
else return null;
}

if (currParent.containsKey(nodes[nodes.length - 1]))
return currParent.get(nodes[nodes.length - 1]);
}
} else if (!contains(key) || (contains(key) && !hasValue(key)))
return null;

return data.get(key);
}

public static FileReader read(File file) {
try {
if (!file.exists())
return null;
return new FileReader(file);
} catch (FileNotFoundException exception) {
exception.printStackTrace();
}
return null;
}

public static boolean load() {
setup();

FileReader reader = read(saveLocation);

if (reader == null) {

data = Collections.synchronizedMap(new LinkedHashMap<String, Object>());
return true;
}

data = Collections.synchronizedMap((Map<String, Object>) yaml.load(reader));

System.out.println(getInteger("Settings.Difficulty.Level"));
System.out.println(getInteger("Settings.Difficulty.Locked"));
System.out.println(getInteger("Settings.Cheats.Enabled"));
System.out.println(getInteger("Settings.Cheats.Locked"));
System.out.println(getInteger("Settings.GUI.Enabled"));
System.out.println(data.toString());

return true;
}
public static void save() {


//Settings
set("Settings.Difficulty.Level", 1);// not saving
set("Settings.Difficulty.Locked", 2);
set("Settings.Cheats.Enabled", 3); // not saving
set("Settings.Cheats.Locked", 4);
set("Settings.GUI.Enabled", 5);

try {
if (!saveLocation.exists())
saveLocation.createNewFile();

FileWriter writer = new FileWriter(saveLocation);

writer.write(yaml.dump(data));
writer.flush();
writer.close();
} catch (IOException exception) {
exception.printStackTrace();
}
}

public static void set(String key, Object object) {
if (!exists())
return;

if (key.contains(".")) {
String[] nodes = key.split("\\.");

// if data doesn't contain top-level node, create nested Maps
if (!data.containsKey(nodes[0])) {
Map<String, Object> currNode = new HashMap<>(), prevNode;
currNode.put(nodes[nodes.length - 1], object);

for (int i = nodes.length - 2; i > 0; i--) {
prevNode = currNode;

currNode = new HashMap<>();
currNode.put(nodes[i], prevNode);
}
// set top-level node to previous parent
data.put(nodes[0], currNode);
} else { // if data contains top-level node, work through each Map
Map[] prevNodes = new LinkedHashMap[nodes.length - 1];

if (nodes.length > 1) {
for (int i = 0; i <= nodes.length - 2; i++) {
if (data.containsKey(nodes[i]) && (data.get(nodes[i]) instanceof Map))
prevNodes[i] = new LinkedHashMap((Map) data.get(nodes[i]));
else if (!data.containsKey(nodes[i]))
prevNodes[i] = new LinkedHashMap();
}

prevNodes[prevNodes.length - 1].put(nodes[nodes.length - 1], object);

for (int i = prevNodes.length - 1; i >= 1; i--)
prevNodes[i - 1].put(nodes[i], prevNodes[i]);

data.put(nodes[0], prevNodes[0]);
} else data.put(nodes[0], object);
}
return;
}
data.put(key, object);
}

public static void main(String[] args) {
path = "test.txt";
setup();
save();
load();
}
}


Normally it'll save integers from a bunch of other classes, then when the program is run again, loads it back into the appropriate variables. But for the sake of testing, all it does is print out the values.

//Settings
set("Settings.Difficulty.Level", 1);// not saving
set("Settings.Difficulty.Locked", 2);
set("Settings.Cheats.Enabled", 3); // not saving
set("Settings.Cheats.Locked", 4);
set("Settings.GUI.Enabled", 5);


This should save integers (1 to 5) to test.txt, and then prints them to the console

System.out.println(getInteger("Settings.Difficulty.Level"));
System.out.println(getInteger("Settings.Difficulty.Locked"));
System.out.println(getInteger("Settings.Cheats.Enabled"));
System.out.println(getInteger("Settings.Cheats.Locked"));
System.out.println(getInteger("Settings.GUI.Enabled"));


When I run the file, I expect to get the output of


1

2

3

4

5


But I actually get


-1

2

-1

4

5


Settings.Difficulty.Level
and
Settings.Cheats.Enabled
doesnt even seem to be saved in the file at all.

Settings:
Difficulty:
Locked: 2
Cheats:
Locked: 4
GUI:
Enabled: 5


I'm pretty sure the issue is somewhere in the
set
method, but I'm not sure exactly what it is. I also don't know why its only two of the 5 variables that won't save (Must be something to do with the name used).

Answer

The error is here:

for (int i = 0; i <= nodes.length - 2; i++) {
    if (data.containsKey(nodes[i]) && (data.get(nodes[i]) instanceof Map))
        prevNodes[i] = new LinkedHashMap((Map) data.get(nodes[i]));
    else if (!data.containsKey(nodes[i]))
        prevNodes[i] = new LinkedHashMap();
}

This code is executed with the second set call. It finds "Settings" in data, but then, it does not find "Difficulty" in data (because that is in the "Settings" map). So it creates a new, empty LinkedHashMap. Then, this code happens:

prevNodes[prevNodes.length - 1].put(nodes[nodes.length - 1], object);

for (int i = prevNodes.length - 1; i >= 1; i--)
    prevNodes[i - 1].put(nodes[i], prevNodes[i]);

It replaces the existing "Difficulty" node (which contains "Level") with the newly created one (which contains only "Locked"). Thus, "Level" is missing afterwards.

You can modify your for loop like this:

for (int i = 0; i <= nodes.length - 2; i++) {
    final Map cur = (i == 0) ? data : prevNodes[i - 1]; 

    if (cur.containsKey(nodes[i]) && (cur.get(nodes[i]) instanceof Map))
        prevNodes[i] = new LinkedHashMap((Map) cur.get(nodes[i]));
    else if (!cur.containsKey(nodes[i]))
        prevNodes[i] = new LinkedHashMap();
}

But actually, the whole set method can be simplified: (Beware, this is untested code and may contain minor errors)

public static void set(String key, Object object) {
    if (!exists())
        return;

    // no need to differentiate between paths with and without "."
    final String[] nodes = key.split("\\.");

    Map cur = data;

    for (int i = 0; i <= nodes.length - 2; ++i) {
        Object val = cur.get(nodes[i]);
        if (val == null) {
            val = new LinkedHashMap();
            cur.put(nodes[i], val);
        } else if (!(val instanceof Map)) {
            // error in structure, handle it here (throw exception, …)
        }
        cur = (Map) val;
    }
    cur.put(nodes[nodes.length - 1], object);
}