Simplify Switch Statements with Maps and Actions

Created: 22.02.2014

Parsing files by keywords in a generic way with actions

Sometimes you have to parse files line by line. For example, some sort of configuration file or a "disassembled" pdf document. The following solution is not for well structured key/value files, like connection.properties. There are easier solutions for this kind of parsing.

In this scenario, you might want to execute another piece of code for each line depending on the first (key-) word. A naive approach would look like this:

String[] parts;
while ((line = reader.readLine()) != null) {      
	parts = line.split("\\s");
	if(parts.length == 0) 
    	continue; //skip empty rows            
    if(parts[0].equals("SET")){           
		//do something      
	} else if(parts[0].equals("PORT")){
		//do something else      
	}
	//...  
}

This kind of code has two downsides. First of all, if the line starting with "SET" is processed, that if clause will never be reached again. But the condition will be checked every time the while loop starts another round. Second, the code explodes if you have lots of keys.

Solution with an action map

I recommend to use a pattern that has allready proven well in web applications. A FrontController typically has an action map that maps URLs to delegate the requests other fine-grained controllers. You can adapt it like described below.

First, you need to add an action map as attribute to your class (it could be static or not, depending on the actions .. in my case its not static because it modifies other non static attributes).

private Map<String, ParseAction> actionMap;

A ParseAction is an abstract class (I definied it local als private class, adapt the scope for your own needs). It has just one method which parses the current line and gets the current line number as an extra information.

private abstract class ParseAction{  		
	public abstract boolean parse(String[] parts, int lineNumber) throws ParseException;  	
}  	  	
private class StringParseAction extends ParseAction{  		
	private String parseResult;
	public StringParseAction(String parseResult){  			
		this.parseResult = parseResult;  		
	}  		
	@Override
	public boolean parse(String[] parts, int lineNumber) throws ParseException{  			
		if(parts.length < 2) 
        	throw new ParseException("Error in line " + lineNumber + "no value found (line is empty)", lineNumber);  			
		parseResult = parts[1];
		return true; //success  		
	}  	
}  	  	
private class FlagParseAction extends ParseAction{  		
	private boolean flag;  		
	public FlagParseAction(boolean flag){  			
		this.flag = flag;  		
	}  		
	@Override  		
	public boolean parse(String[] parts, int lineNumber) throws ParseException{  			
    	//if key matches, the flag can be set true  			
        flag = true;  			
        return true;  		
	}  	
}

I added two subclasses for ParseAction. The StringParseAction takes a string attribute (parseResult) during construction. It modifies this string attribute once the parse method is called. The string attribute is an attribute of the class that parses the file.

Sometimes you have such simple actions like the ones above. If thats the case for all lines, you could really use a key/value like approach. But once the parse method becomes more complex and includes some consistency checks (when you can't rely on the "well-formedness" of the file), you will need a more robust approach like this.

The other subclass is a variant. It sets a flag, if a certain key exisist in a line of the file.

If you need some extra flexibility and access to other attributes inside you action classes, you can provide the wrapping class in the constructor of the action class:

private class StringParseAction extends ParseAction{  
	private FileParser parser;
	private String parseResult;
	
    public StringParseAction(FileParser parser, String parseResult){
    	this.parser = parser;			      
		this.parseResult = parseResult;  
	}    
	@Override  
	public boolean parse(String[] parts, int lineNumber) throws ParseException{      
		if(parts.length < 2) 
			throw new ParseException("Error in line " + lineNumber + "no value found (line is empty)", lineNumber);  			      
		if(parser.getSomeAttribute().equals("XY")){          
			parseResult = parts[1];      
		} else {
			parseResult = null;      
		}      
		return true; //success  
	}  
}

Now you are ready to init the map:

private void initActionMap(){  	
	actionMap = new HashMap<>();  		  	
	actionMap.put("SET", new StringParseAction(setValue));  	
	actionMap.put("PORT", new StringParseAction(portValue));  	
	actionMap.put("COMPOUNDFLAG", new FlagParseAction(compoundFlag));          
	//..  
}

Your parsing code will be much simpler now:

String[] parts;  
while ((line = reader.readLine()) != null) {      	
    parts = line.split("\\s");      	
    if(parts.length == 0) 
        continue; //skip empty rows  		    	      	
    try{      		
        actionMap.get(parts[0]).parse(parts, lineNum);
    } catch (ParseException e) {   		
        e.printStackTrace();          
    }  
}