Mark is a software developer on Team RateView with TravelClick (http://www.TravelClick.net/). Mark can be reached at [email protected].
I recently worked on a project that required reading standard Microsoft-style .ini files in Java. Although the JDK 1.4 does not specifically provide an .ini class, I thought that either the JDK 1.4 java.util.Properties or the Preferences API would do the job. I quickly discovered, however, that neither cleanly translates to the standard .ini file format. By "standard" .ini file format, I mean a format similar to Microsoft's .ini file format, where text entries are organized by sections, each section with corresponding key/value pairs separated by an equals sign (=), and comments that use semicolons (;) and pound signs (#); see Example 1.
With this in mind, I created IniFile, a lightweight Java class that reads standard .ini text files into memory, lets you manipulate their contents, and store the various sections with their corresponding key/value pairs in memory.
IniFile (available electronically; see "Resource Center," page 5) has one public constructor, which takes two parameters. The first parameter is a String object pointing to a disk file; and the second parameter is a Boolean indicating whether the IniFile class should be case sensitive. If this parameter is False, then IniFile turns case sensitivity off (see Listing One). After calling the IniFile constructor, you call the loadFromFile() method (Listing Two) to load the contents of an .ini file in to memory. Calling the IniFile constructor and the loadFromFile() method on a file that does not exist causes no problems. In fact, this is what you need to do to create new files right from the start.
Working with IniSections
After loading IniFile (be it empty or not), you can create a new section or read from an existing section in the IniFile object. The IniFile class relies upon another classIniSection (available electronically). Just as a typical .ini file has one or more sections designated by square brackets [MySection], the IniFile class creates one IniSection object for each section it finds in the file. If you want to add a section to your in-memory IniFile object, invoke the addSection() method, passing in the name of the section identifier you want to create:
inf.loadFromFile();
iniSection sect = inf.addSection("fruit");
As Figure 1 illustrates, IniFile also provides public methods to delete a section (public void deleteSection(String value)), get a pointer to an existing section (public IniSection getSection(String name)), get a collection of all sections (public Set getSections()), and query as to whether a given section exists (public boolean hasSection(String value)).
The IniSection class encapsulates a specific section in a given .ini file. A typical section has one or more key/value pairs that are separated by an equals sign:
[fruit]
banana = yellow
The IniSection class (Figure 2) provides accessor methods to delete a key (public void deleteKey(String key)), get a collection of the keys in a section (public Set getKeys()), get a value given a key (public String getValue(String key)), get a collection of values (public Set getValues()), determine whether a key exists (public boolean hasKey(String key)), determine whether a value exists (public boolean hasValue(String value)), set the value of key/value pairs (public void setValue (String key, String value)), and return the section represented as a property of java.util.Properties (public Properties getProperties()).
Writing to Disk with IniFile
IniFile works with the contents of an .ini file in memory after calling the loadFromFile() method. When you're ready to write changes to disk, simply invoke the flushToFile() method (Listing Three), and IniFile does the rest. This scratch-pad ability also lets you load the contents of one file, make changes in memory, then write the contents out to another file.
The IniFile class relies on a LinkedHashMap class to maintain an ordered list of its sections. I chose LinkedHashMap because, according to JDK 1.4, iteration, ordering is defined and is normally the order in which keys are inserted into the LinkedHashMap object. Just as with IniFile, the IniSection object utilizes a LinkedHashMap class to maintain its list of key/value pairs.
When IniFile parses a file into memory, it creates an IniSection object for each section it finds in the file. It then populates the IniSection object by calling the setValue() method, passing in the key and value. The setValue() method (Listing Four) is simply a wrapper method that first checks to see whether case sensitivity is on, then checks whether the key already exists in the internal LinkedHashMap _map. If the key does not exist, it calls the put() method of the LinkedHashMap class (available electronically).
When an IniSection has been filled in, the IniFile class calls the put method of its LinkedHashMap (_sectionMap), passing in the name of the section and the now-filled-in IniSection object:
_sectionMap.put(name, currentSection);
Maintaining Anonymity
While first storing all the elements of an IniSection object in a LinkedHashMap, then storing the IniSection objects in another LinkedHashMap, makes things easy for the IniFile class, I still needed a mechanism to flag lines that contained white space and comments. I also needed a means of storing these special lines in the order they appeared in the file and writing them back out to a filewhile not having to worry about them during retrieval processes. The trick is to treat comment and whitespace lines as if they're keys and sections, but still make them anonymous so they cannot be retrieved.
For instance, if IniFile found:
1 # comment 1
2 # comment 2
3 # comment 3
4 [MyFirstSection]
at the top of a file, it would interpret and store lines 1-3 as anonymous sections. It doesn't matter to IniFile that the anonymous sections do not have associated key/value pairs. Line 4 would be stored as a legitimate section line.
In Listing Five, IniFile treats lines 1-4 as anonymous sections, even though two of the lines are whitespace lines (just like the comment lines). Again, line 5 is accepted as a legitimate section line, and lines 6-7 are stored as anonymous keys under a legitimate section tag [MyFirstSection]. Finally, lines 8-9 are stored as legitimate keys, and line 10 as another legitimate section.
The Not So Obvious
During testing, I searched my hard drive for files with a mask of "*.ini," in hopes of finding some large and complicated examples that would produce good test cases. Out of the gate, I found OPCTRNM.INI located in the C:\ORANT\DBS directory. When I opened it with TextPad, I found C++-style comment lines similar to Figure 3even though you only use semicolons or pound signs to indicate a comment in .ini files. The last thing I wanted to do was to change code every time a new comment string indicator came along. As luck would have it, I was working with javaCC (http://www.webgain.com/products/java_cc/), which lets you build parser logic. As it turns out, the syntax for defining acceptable token values for the parser (Example 2) is exactly what I needed. By applying this concept to my IniFile class, users can dynamically define any comment indicator they want. To provide this functionality, I use a java.util.HashMap class named _commentTable to store the comment indicators. Access to _commentTable is provided via the public method addComment. Comments are defined in the HashMap table as name/value pairs (see Listing Six). Now, with one method invocation, you can define any comment indicator string (Listing Seven).
Finally, I set up a Boolean flag in the constructor (Listing Eight) to provide case sensitivity. This flag allows decision making to be done when calling internal methods such as hasKey() and setValue().
Conclusion
There are any number of things you can do to make IniFile more useful. For instance, try using a switch to the IniFile constructor, letting it automatically load "default" comments into the comment table. This would prevent users of the class from having to use the loadComment() method each time.
DDJ
Listing One
// constructor public IniFile( String filePathName, boolean caseOn ) throws IOException { this.setCaseOn(caseOn); this.setLineNumberRead(filePathName); } // turn case sensitivity off IniFile inf = new IniFile(args[0],false);
Listing Two
// load the contents of the file passed in to the contructor inf.loadFromFile();
Listing Three
// create IniFile object from mark.ini IniFile inf = new IniFile("c:\\mark.ini",false); inf.loadFromFile(); // add a section in memory IniSection sect = inf.addSection("fruit"); // add a key+value pair under the "fruit" section sect.setValue("banana","yellow"); // write contents of memory out to mega.ini inf.setFileName("c:\\mega.ini"); inf.flushToFile();
Listing Four
currentSection = new IniSection( name, this.getCaseOn() ); public void setValue (String key, String value){ String s; if (getCaseOn()){ s = key.toUpperCase(); } else{ s = key; } if (!this.hasKey(key)){ _map.put( s, value ); } }
Listing Five
1 # comment 1 2 <white space here 3 # comment 2 4 <white space here 5 [MyFirstSection] 6 <white space here 7 ; comment = 3 very bogus 8 banana = yellow 9 apple = red 10 [MyNextSection]
Listing Six
private HashMap _commentTable = new HashMap(); public void addComment(String name, String value){ _commentTable.put(name,value); }
Listing Seven
IniFile inf = new IniFile("c:\mark.ini",false); inf.addComment("POUND","# "); inf.addComment("SEMICOLON",";"); inf.addComment("DOUBLESLASH","//");
Listing Eight
public void setValue (String key, String ue){ String s; if (getCaseOn()){ s = key.toUpperCase(); } else{ s = key; } if (!this.hasKey(key)){ _map.put( s, value ); } }