public static InterpreterData parseFile(String fileName) {
		InterpreterData result = new InterpreterData();
		result.ruleNames = new ArrayList<String>();

		try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
		    String line;
		  	List<String> literalNames = new ArrayList<String>();
		  	List<String> symbolicNames = new ArrayList<String>();

			line = br.readLine();
			if ( !line.equals("token literal names:") )
				throw new RuntimeException("Unexpected data entry");
		    while ((line = br.readLine()) != null) {
		       if ( line.isEmpty() )
					break;
				literalNames.add(line.equals("null") ? "" : line);
		    }

			line = br.readLine();
			if ( !line.equals("token symbolic names:") )
				throw new RuntimeException("Unexpected data entry");
		    while ((line = br.readLine()) != null) {
		       if ( line.isEmpty() )
					break;
				symbolicNames.add(line.equals("null") ? "" : line);
		    }

		  	result.vocabulary = new VocabularyImpl(literalNames.toArray(new String[0]), symbolicNames.toArray(new String[0]));

			line = br.readLine();
			if ( !line.equals("rule names:") )
				throw new RuntimeException("Unexpected data entry");
		    while ((line = br.readLine()) != null) {
		       if ( line.isEmpty() )
					break;
				result.ruleNames.add(line);
		    }

			line = br.readLine();
			if ( line.equals("channel names:") ) { // Additional lexer data.
				result.channels = new ArrayList<String>();
			    while ((line = br.readLine()) != null) {
			       if ( line.isEmpty() )
						break;
					result.channels.add(line);
			    }

				line = br.readLine();
				if ( !line.equals("mode names:") )
					throw new RuntimeException("Unexpected data entry");
				result.modes = new ArrayList<String>();
			    while ((line = br.readLine()) != null) {
			       if ( line.isEmpty() )
						break;
					result.modes.add(line);
			    }
			}

		  	line = br.readLine();
		  	if ( !line.equals("atn:") )
		  		throw new RuntimeException("Unexpected data entry");
			line = br.readLine();
			String[] elements = line.substring(1,line.length()-1).split(",");
	  		int[] serializedATN = new int[elements.length];

			for (int i = 0; i < elements.length; ++i) { // ignore [...] on ends
				serializedATN[i] = Integer.parseInt(elements[i].trim());
			}

		  	ATNDeserializer deserializer = new ATNDeserializer();
		  	result.atn = deserializer.deserialize(serializedATN);
		}
		catch (java.io.IOException e) {
			// We just swallow the error and return empty objects instead.
		}

		return result;
	}
The structure of the data file is very simple. Everything is line based with empty lines
separating the different parts. For lexers the layout is:
token literal names:
...

token symbolic names:
...

rule names:
...

channel names:
...

mode names:
...

atn:
<a single line with comma separated int values> enclosed in a pair of squared brackets.

Data for a parser does not contain channel and mode names.
runtime/Java/src/org/antlr/v4/runtime/misc/InterpreterDataReader.java