Controlling Yourself. A Framework for Configurable Options
Written by John Holt
Most programs have a set of persistent options that are under user control. These options must be managed. This article discusses an approach to uniform management of option sets that may reside in a profile (INI file) or on a file's extended attributes (EA's).
The home for user specified general application defaults varies by system. In a UNIX system, environment variables or entries in .mwmrc (the Motif resource file) are used for system-wide defaults. In Windows and OS/2, INI files are used to hold the general application defaults. The OS/2 file system also provides an additional means of keeping file specific default settings.
Problem and Environment Description
Any general framework of classes must have certain attributes. Two attributes that are germane to this article are programmer-extensibility and harmony with the external environment. The facility for programmer extensions is simply providing a capability for the programmer to add new classes to be stored in the profile or extended attributes. The attribute of harmony with the external environment in this case is using profiles and extended attributes in a way that facilitates the use of existing tools.
Several base and implementation classes were developed. The task of managing persistent option sets is common to many programs, and therefore is an excellent candidate for reusable objects. The classes developed for the article represent an attempt to divide the implementation into classes so as to reuse common behaviors. The model for implementation is individual objects grouped into a set. The set is then stored on either a profile or in EAs.
Class Relationship Structure
The class structure is depicted in the diagram below. The relationship marked IS-A denotes class derivation. The relationship marked HAS-A shows that the class contains an object of the class. The KeyedOptionSet class is the result of a template generation using the KeyedSequentialSet class in the compiler collection classes library.
The option set behaviors have been factored into a base class (BOptionSet) and two implementation classes (INIOptionSet and EAOptionSet). The behaviors specific to manipulation of EAs or profiles is in EAOptionSet and INIOptionSet respectively.
BOptionSet is concerned with the maintenance of the list of items to be stored or retrieved. The following public methods are implemented:
- Adds an item in the form of a pointer to a BOptionItem and a name for the option
- Removes an item from the set
- The character string used as the high order name qualifier for EAs or the application name parameter for INI calls.
- The virtual destructor used to clean up.
The BOptionSet object implements several protected methods used by the derived classes to access and manipulate the private data members. Those behaviors are:
- Position the set cursor to the first item
- Advance the set cursor to the next item in the set
- Returns the pointer to an item in the set that is at the current cursor position.
- Makes this set a copy of another set.
- Returns the number of bytes needed to store the item key names.
- Returns the number of bytes needed to store a flat version of the set of items.
- Returns the number of items in the set.
- The object constructors. There is a copy constructor and a constructor taking the application name and producing an empty set.
The KeyedOptionSet is generated from the IKeySet template class which is a keyed sequential set class in the IBM collection classes. A cursor is provided by the template. The object is a set of OptionSetElem objects.
The OptionSetElem is a simple object holding only the name of the option and a pointer to a BOptionItem object.
The derived classes EAOptionSet and INIOptionSet provide the implementations for the loadSet, storeSet, and resetSet as well as the usual expected complement of constructors and an equal operator.
The loadSet method updates each item with the value stored for that item's name. For the case of the items stored as EAs, the name is <application name>.<item name> and is mono-case. For objects stored in profiles, the name is forced to be in uppercase for consistency. The storeSet method is the reverse of the loadSet.
The resetSet method performs two operations. First, the set of names in the profile or EAs is made to correspond with the set of names in the set. Any names with a prefix other than <application name> are ignored. The second operation is to perform a storeSet.
The BOptionItem class provides some default implementations for the behaviors. The class interface is primarily intended to be the communication portal between an option set and the real data objects that are the elements of that set. The interface is:
- Returns an enum indicating that the object is a character string or is binary data
- Returns the size of the item in bytes
- Returns a flattened version of the item consisting of a length and a pointer to a string of bytes.
- Accepts a flattened version of the item for updating the real data object.
The StrOptionItem is a class definition for a string data object. The definition provides for the normal string behaviors.
Interface to Programmer Supplied Objects
The interface used by the option set to manipulate the items in memory is BOptionItem. Because all option items are derived from BOptionItem, the correct method is invoked.
When an addItem method is invoked on a set, the set obtains a BOptionItem type pointer to that item. When the set needs the size of the item, e.g. to allocate a buffer, the pointer to a BOptionItem is used to invoke the correct method. Similarly, the importData and exportData methods are invoked to move the value to and from the persistent store.
Adding New Object Classes
There are two different strategies that can be employed for extending the range of types of items that can be used in option sets. The programmer may implement interface behaviors directly in the object to be added, or the programmer can implement a wrapper class. If the class is under control of the programmer, direct implementation is usually the best approach. If the object to be used is provided by a third party class library, or is not a first class object (e.g., an INT) then a wrapper class is the right approach. The sample program uses the later approach because it is the most non-obvious of the two approaches. The reader will notice that the example is contrived.
Walkthrough the Sample/Test Program
The sample/test program illustrates how to create a set, add option items to that set, and store the set of option items. To exercise the code, these tasks were performed for both profile and EA based option sets. The file test.cpp [see test_src.zip within options.zip - Editor] should be followed for this discussion. You may also want look at BOptionSet.cpp and StrOptionItem.cpp [see source.zip within options.zip - Editor] for additional detail.
|Housekeeping||test.cpp||Initialize several variables, get an anchor block, and open the test profile|
|create empty set||test.cpp||Declare object of type INIOptionset with a string and handle for the profile|
|INIOptionSet.cpp||Constructor for string and handle pass string to the BOptionSet for the application name, then hold onto the handle for subesquent file operations|
|create items||test.cpp||Declares for a StrOptionItem and two Option items. Option is a contrived class defined in the file option.h. Both of these objects take a value an d a string
used as the name of the option. The name is passed on to the BOptionItem parent class.
|Add items to the set||test.cpp||The addItem method is invoked for each item|
|BOptionSet.cpp||The addItem method allocates an OptionSetElem object to hold the
BOptionItem * and the name of the item. The element is then added to the set or replaces a previous item of the same name. The size of the item name is recorded.
|store the set||INIOptionSet.cpp||The items are stored on the profile test.ini. The set is traversed and each item is individually written to the profile. The exportdata method
is invoked for the BOptionItem involved. In the case of Item3, StrOptionItem: :exportdata is invoked
|StrOptionItem.cpp||A FlatItem data structure is allocated along with a buffer to hold the string value. The pointer to this is returned. It will be the callers responsibility to free the areas.|
|change the values of the items||test.cpp||The item values are change via the operator= method|
|reset the items from the store||test.cpp||The loadset method is invoked to update the items from the values held in the INI file. This restores them to their original values.|
|INIOptionSet.cpp||For each item in the option set: the size of the item is obtained
via the PrfQueryProfileItemSize() call; the item is read from the profle; the importData method for that item is invoked to update the item value in memory
|StrOptionItem.cpp||The importData method receives a FlatItem structur. The current
string dat is freed, and the new value is kept.
|delete some items||test.cpp||Items 1 and 2 are deleted from the set, and the version of the set on the profile is update via resetSet so that only Item3 remains.|
|Housekeeping||test.cpp||The profile data set is closed, and a regular sequential file is
opened for the subsequent EA object testing.
|Create an EAOptionSet||test.cpp||The EAOptionSet constructor is invoked, using the INIOptionSet object from above, amd the file handle for the sequential dataset opened above|
|EAOptionSet.cpp||The constructorsends the set name to the BOptionSet constructor
for later use as the applicaion name. The handle of the file is retained.
|reset EA's to known values||test.cpp||The resetSet operation is invoked which eliminates all option items currently stored as EAs, and replaces them with the current set contents, in this case, Item3.|
|Exercise the EAmethods||test.cpp||Items are added to the set; the set is stored; values are
changed and reset in a manner similar to the tests for the INIOptionSet.
There are several things worth keeping in mind. The flow for the above example is designed to test the logic, and not how the objects would be used in a real application. In a real application, the steps for use would be:
- The data objects for all of the program options are declared, and initial values are given. All of these items must be derived from BOptionItem. The object may be derived from more than one class.
- The INIOptionSet item is declared using either the handle to an INI file just for this application, or the handle to the user ini file (HINI_USERPROFILE).
- The loadSet operation is invoked to pick up any defaults established by the user.
If there are file specific options, then the EAOptionSet object is used as well. The EAOptionSet object would be created from the INIOptionSet object, and if there were any non-file related options, those option items would be removed from the EAOptionSet via the delItem operation.
At an appropriate time, the option sets would be written to either the profile or the EAs or both.
This section discusses some of the less obvious aspects of the set classes. To warrant the appellation of "less obvious," it had to be something I needed some effort to figure out. Which of course means that they may be completely obvious to you.
The API for the profile was just a joy to use (compared to EAs), and the documentation makes everything pretty clear.
The EAs are not at all the same story. I used two books, and the documentation, to puzzle my way through how EAs worked. The books used were The Art of OS/2 2.1 C Programming, by Panov, Salomon, and Panov; the other book was OS/2 2.1 Application Programmers Guide, by Kelly et. al.
The EAOptionSet operations work on all of the EAs at once. That way there are no windows where a a particular EA may change, or the ordinal positions of the EAs change.
The layout of the EA buffer is as follows:
- The head for each item in the buffer. The cbName contains the length of the name field (NULL terminated) and cbValue contains the length of the data portion. The offset to the next item is sizeof(FEA2) + cbValue + cbName + this position + enough to get you to the next mod 4 offset. The value of the cbValue element is the size of the flat data item + two SHORTs.
- At FEA2 + cbName + 1 is the EAT value. In our case it is either EAT_BINARY or EAT_ASCII. There are other options available, but the remainder of this discussion would not apply.
- The is the size in bytes of the flattened data item
This article has present a set of classes that can be used to manage an application's set of options items. The basic model is that there exists a set of default values for the options of an application; the user is permitted to modify those options in a persistent manner; and that there may be some sub-set of options that are associated with particular files.
There are several obvious features lacking which are required for an industrial strength implementation. In the case of EAs, there are no checks for exclusive control while writing or reading the set of EAs from the file. In the realm of memory management, there are no safeguards against bad pointers.
One extension worthwhile making is that BOptionItems could have reference counts and perhaps references lists of the option sets where they are members.
Useful Test Tools
There are a couple of REXX scripts included in the zip file. In addition, there are several very useful tools at ftp-os2 such as EABrowse. The REXX script to print EAs only works when the the test program has closed the file, but I have no idea why I could not get it to share read access.