Parsing a string with named sections and a key-value pair on each line

I have a response String as shown below which I need to parse it and store it my class. Format is shown below:

• task name followed by this dotted line ------------- which is also fixed
• and then just below key:value pair. It can have many key value pair

Below is the response string.

abc-------------
Peak:4932152

def-------------
Peak:4932216

ghi-------------
Peak:4874588

pqr-------------
Peak:4874748


Below is my class:

public class NameMetrics {

private String name;
private Map<String, String> metrics;

// setters and getters

}


In the above class, name should be abc and metrics map should have Load as the key and 79008 as the value and same with other key:value pairs. Below is the code I have.

String response = restTemplate.getForObject(url, String.class);
NameMetrics current = null;
Map<String, String> metricsHolder = null;
List<NameMetrics> result = new ArrayList<>();
while (true) {
if (s == null) {
break;  // end reached
}
if (s.trim().isEmpty()) {
continue;  // Skip empty line
}
int cut = s.indexOf(':');
if (cut == -1) {
cut = s.indexOf('-');
if (cut == -1) {
continue;
}
metricsHolder = new HashMap<String, String>();
current = new NameMetrics();
current.setName(s.substring(0, cut));
} else if (current != null) {
metricsHolder.put(s.substring(0, cut), s.substring(cut + 1));
current.setMetrics(metricsHolder);
}
}


I'd like a code review to see whether this can be improved in any way?

Your current code is very good: it reads easily, it is properly indented and clean. I am sure one could come up with a complicated regular expression for parsing this String but using a StringReader like you have done is simple and effective. It is dynamic with regard to the number of elements to put in the map.

I only have a couple of comments:

• You are trimming each line only to determine if they are blank with

if (s.trim().isEmpty()) {
continue;  // Skip empty line
}


If you expect lines to have spaces to trim then you could do it all the time.

• Instead of having a setMetrics method that takes a map, I would have a addMetric method that adds a key/value pair into the map.

public void addMetric(String key, String value) {
metrics.put(key, value);
}


This way, you don't need to build the map yourself in the while loop.

• To prevent from NullPointerException, it is a good idea to always initiaze maps and lists when they are in instance variables. You can do it inline like the following:

public class NameMetrics {

private String name;
private Map<String, String> metrics = new HashMap<>();

}


or in the default constructor.

• The variable s in String s = reader.readLine(); could have a better name, like line.
• The while(true) loop with an explicit break works correctly, but it might be easier to follow with the traditional pattern while ((line = reader.readLine()) != null) {.

With those couples changes, you can have:

BufferedReader reader = new BufferedReader(new StringReader(response));
NameMetrics current = null;
List<NameMetrics> result = new ArrayList<>();
String line;
line = line.trim();
if (line.isEmpty()) {
continue; // Skip empty line
}
int cut = line.indexOf(':');
if (cut == -1) {
cut = line.indexOf('-');
if (cut == -1) {
continue;
}
current = new NameMetrics();
current.setName(line.substring(0, cut));
} else if (current != null) {
}
}


First, I'd like to point out that if are you storing this from your program and reading it back from a file later, you'd be better off Serializing it. But since you're being given this file, let's move on.

Infinite Loops

It's generally discouraged to use while(true) loops. It's not particularly bad (in fact it's sometimes better), but it's always best to look for an alternative for readability sake. Instead, let's use while ((line = reader.readLine()) != null).

Initializing variables as null

I also see you initialized a few variables as null, but that isn't necessary since it's implied for classes. (Also note primitive variables are implied as 0 during initialization).

You also have a cut variable that gets the index of the tokens, and then skips the line if it's blank. However, we know that the output is always going to be grouped into three lines, and we always know what those lines are going to be. We also know the data you really want is going to be the first three characters for the name, and everything after the first five characters for the tokens. Let's use that and make everything clear!

Putting it all together

String response = restTemplate.getForObject(url, String.class);
NameMetrics current;
Map<String, String> metricsHolder;
List<NameMetrics> result = new ArrayList<>();

String nameline;
name = nameLine.substring(0, 3);

current = new NameMetrics();
metricsHolder = new HashMap<>();    //No need to repeat the stuff inside the diamond operator
metricsHolder.put("Peak", peak);

current.setName(name);
current.setMetrics(metricsHolder);
}


Handling varying keys and names

When there are a variable number of keys, we still know that they're all generally in the same format of the name followed dashes, then a list of key:value lines, then a blank line. We can still use this to our advantage as follows:

String name;
String[] mapValues;

final int KEY = 0, VALUE = 1;
String nameline, keyLine;
while ((nameline = reader.readLine().trim()) != null) {    //Loop until the end of the file
name = nameLine.substring(0, nameLine.indexOf("-") + 1); //Plus because substring excludes last index

metricsHolder = new HashMap<>();    //No need to repeat the stuff inside the diamond operator
while (!(keyLine = reader.readLine().trim()).isEmpty()) {   //Loop until block ends with an empty line
metricsHolder.put(mapValues[KEY], mapValues[VALUE]);
}

current = new NameMetrics();
current.setName(name);
current.setMetrics(metricsHolder);

• In actual example, it can have more than two values that I need to put in the map. Load and Peak was just an example, it can have more key:value pairs. – user1950349 May 1 '16 at 20:23
• I don't know what keys might be beforehand so it has to be dynamic. In general depending on response variable, it should load key:value pairs in a map, no matter how many are there. – user1950349 May 1 '16 at 20:25