Jump to content

Manage Your Configuration Files and Data: Difference between revisions

From EDM2
Prokushev (talk | contribs)
No edit summary
 
Ak120 (talk | contribs)
mNo edit summary
 
(10 intermediate revisions by 5 users not shown)
Line 1: Line 1:
Written by [[Stefan Ruck]]
''Written by [[Stefan Ruck]]''


[NOTE: Here is a link to a zip of the <a HREF="confsrc.zip">source
[NOTE: Here is a link to a zip of the http://www.edm2.com/0509/confsrc.zip source code for this article. Ed.]
code</a> for this article. Ed.]


<h2>Introduction</h2>
== Introduction ==
Almost every program needs some configuration data. But where to store it? How to save and retrieve the data?


<p>Almost every program needs some configuration data. But where to store it?
In this article I will show you two different ways to manage your configuration data. One describes a template class which gives you the ability to store your data in a self-defined structure. The other gives you a class which is derived from IProfile. This class conforms to IBM's programming guide.
How to save and retrieve the data?


<p>In this article I will show you two different ways to manage your
When you look at the description of configuration files in IBM's programming guide vol. II [1], the guide is always talking about initialization files. I will make a distinction between configuration and initialization files. When I use 'configuration files' I mean those files which use a self-defined structure to store the data. 'Initialization files' are the files which store the data conforming to the programming guide using the IProfile class or my derived AIniFile class. I will use 'ini file' for the global meaning.
configuration data. One describes a template class which gives you the
ability to store your data in a self-defined structure. The other gives
you a class which is derived from IProfile. This class conforms to IBM's
programming guide.


<p>When you look at the description of configuration files in IBM's
== Where To Place The Data? ==
programming guide vol. II [1], the guide is always talking about
What I hadn't found in the programming guide is a specification about the name of the file and its location. Also IProfile accepts any filename passed to its constructor.
initialization files. I will make a distinction between configuration and
initialization files. When I use 'configuration files' I mean those files
which use a self-defined structure to store the data. 'Initialization
files' are the files which store the data conforming to the programming
guide using the IProfile class or my derived AIniFile class. I will use
'ini file' for the global meaning.


<h2>Where To Place The Data?</h2>
[[Harald Wilhelm]] gave the base definition in his article 'Register me!' [2], which I expanded a little bit. He said that the filename should be the same as the program name except the filename suffix, which should almost always be 'ini'. The sequence for where to place the file was defined as follows:
* The directory of the main program. If this is write protected,
* The current directory. If this is write protected too,
* The system directory (\os2).
I think the second place to look for is the directory where the user ini file (os2.ini) is located. This is set in the config.sys by the environment variable USER_INI. Then follows the current and at last the system directory.


<p>What I hadn't found in the programming guide is a specification about the
Setting the filename suffix I distinguish between the configuration file (suffix 'cfg') and the initialization file (suffix 'ini').
name of the file and its location. Also IProfile accepts any filename
passed to its constructor.


<p>Harald Wilhelm gave the base definition in his article 'Register me!'
I think you should NEVER use the os2.ini file to store your initialization data. There are several reasons why not. The first is a simple question: Have you ever corrupted the registry of your Win95 or NT system? Another point is when you save the configuration in a separate file, it's easy for the user to reconfigure his system without losing his application configurations. And thinking about it a little bit I guess you'll find a few more good reasons.
[2], which I expanded a little bit. He said that the filename should be
the same as the program name except the filename suffix, which should
almost always be 'ini'. The sequence for where to place the file was
defined as follows:


<ol>
== Some Restrictions ==
<li>The directory of the main program. If this is write protected,
As defined above, each application should have its own ini file. This concept is strictly realized in the classes I've written. It is a reduction against the IProfile class and the OS/2 API which gives you the ability to store data for different applications in the same initialization file. The unique data key in these files is always the combination application / user key. You can't pass a filename to the classes I've written. The filename is set by the object itself at runtime.
<li>The current directory. If this is write protected too,
<li>The system directory (\os2).
</ol>
 
I think the second place to look for is the directory where the user ini
file (os2.ini) is located. This is set in the config.sys by the
environment variable USER_INI. Then follows the current and at last the
system directory.
 
<p>Setting the filename suffix I distinguish between the configuration
file (suffix 'cfg') and the initialization file (suffix 'ini').
 
<p>I think you should NEVER use the os2.ini file to store your
initialization data. There are several reasons why not. The first is a
simple question: Have you ever corrupted the registry of your Win95 or NT
system? Another point is when you save the configuration in a separate
file, it's easy for the user to reconfigure his system without losing his
application configurations. And thinking about it a little bit I guess
you'll find a few more good reasons.
 
<h2>Some Restrictions</h2>
 
<p>As defined above, each application should have its own ini file. This
concept is strictly realized in the classes I've written. It is a
reduction against the IProfile class and the OS/2 API which gives you the
ability to store data for different applications in the same
initialization file. The unique data key in these files is always the
combination application / user key. You can't pass a filename to the
classes I've written. The filename is set by the object itself at runtime.
So you can never read the ini file of another application or a global one.
So you can never read the ini file of another application or a global one.


<p>The Class Hierarchy:
The Class Hierarchy:
 
<pre>
<pre><small>
                        AConfigBase
                            AConfigBase
                      (abstract class)
                        (abstract class)
                            |
                                |
              -----------------------------
                -----------------------------
              |                          |
                |                          |
          AConfigFile                  AIniFile
            AConfigFile                  AIniFile
      (abstract class)            (abstract class)
          (abstract class)            (abstract class)
              |                          |
                |                          |
          ACfgConfig                  AIniConfig
            ACfgConfig                  AIniConfig
(application specific class)  (application specific class)
    (application specific class)  (application specific class)
</pre>
</small></pre>
''Figure 1: The ini file class hierarchy.''
 
<font SIZE=2>
Figure 1: The ini file class hierarchy.
</font>
 
<p>AConfigBase is the base class of AConfigFile, which handles
configuration files, and AIniFile, which covers initialization files.
AConfigBase handles building the filename, which is similar for both
AConfigFile and AIniFile.
 
<p>AConfigFile and AIniFile are abstract classes as AConfigBase. This
means you cannot create an object of these classes. You have to derive an
application specific class from AConfigFile or AIniFile for use.


<p>Why is that? In my opinion AConfigFile and AIniFile are still
AConfigBase is the base class of AConfigFile, which handles configuration files, and AIniFile, which covers initialization files.
incomplete. A complete ini class needs an interface to access the
AConfigBase handles building the filename, which is similar for both AConfigFile and AIniFile.
different configuration parameters. An example for complete ini classes
are ACfgConfig and AIniConfig (see below).


<h2>The Base For All Ini Files: AConfigBase</h2>
AConfigFile and AIniFile are abstract classes as AConfigBase. This means you cannot create an object of these classes. You have to derive an application specific class from AConfigFile or AIniFile for use.


<p>The class AConfigBase is defined in cfgbase.hpp and cfgbase.cpp.
Why is that? In my opinion AConfigFile and AIniFile are still incomplete. A complete ini class needs an interface to access the different configuration parameters. An example for complete ini classes are ACfgConfig and AIniConfig (see below).


<pre><small>
== The Base For All Ini Files: AConfigBase ==
   class AConfigBase : public IBase
The class AConfigBase is defined in cfgbase.hpp and cfgbase.cpp.
<code>
   class AConfigBase:public IBase
   {
   {
     public:
     public:
       AConfigBase (const char * pszSuffix);
       AConfigBase (const char* pszSuffix);
 
       virtual ~AConfigBase () = 0;
       virtual ~AConfigBase () = 0;
 
       const IString& getFullFilename ()
       const IString& getFullFilename ()
                     { return (m_strFullFilename); }
                     { return (m_strFullFilename); }
Line 120: Line 65:
       const IString& getPathname ()
       const IString& getPathname ()
                     { return (m_strPathname); }
                     { return (m_strPathname); }
 
   private:
   private:
     AConfigBase& filenameFromAppDir (const char * pszSuffix);
     AConfigBase& filenameFromAppDir (const char * pszSuffix);
                           // build filename from app directory
                           // build filename from app directory
                   // throws IAccessError
                   // throws IAccessError
 
       APIRET filenameFromIniDir ();
       APIRET filenameFromIniDir ();
                           // build filename from ini directory
                           // build filename from ini directory
 
       APIRET filenameFromCurrDir ();
       APIRET filenameFromCurrDir ();
                       // build filename from current directory
                       // build filename from current directory
 
       AConfigBase& filenameFromSystemDir ();
       AConfigBase& filenameFromSystemDir ();
                                 // build filename from system
                                 // build filename from system
                                 // directory
                                 // directory
                   // throws IAccessError
                   // throws IAccessError
 
       AConfigBase& buildPathname ();
       AConfigBase& buildPathname ();
                 // build the pathname from m_strFullFilename
                 // build the pathname from m_strFullFilename
 
       Boolean tryOpenFile (AFile& rFile, IString& rstrFile);
       Boolean tryOpenFile (AFile& rFile, IString& rstrFile);
               // throws IAccessError
               // throws IAccessError
 
       IString m_strFullFilename,  // filename including path
       IString m_strFullFilename,  // filename including path
               m_strPureFilename,
               m_strPureFilename,
               // filename without path, but with leading '\'
               // filename without path, but with leading '\'
               m_strPathname;      // only the path
               m_strPathname;      // only the path
 
       APIRET m_ret;
       APIRET m_ret;
                 // keeps the return value of the API calls
                 // keeps the return value of the API calls
   };
   };
</code>
''Figure 2: Declaration of AConfigBase.''


</small></pre>
The constructor of AConfigBase needs the suffix of the ini file as a parameter to be able to build the complete filename. It is passed by AConfigFile and AIniFile during the initialization of these classes.


<font SIZE=2>
The destructor is declared as pure virtual to make AConfigBase an abstract class.
Figure 2: Declaration of AConfigBase.
</font>


<p>The constructor of AConfigBase needs the suffix of the ini file as a
The three public methods are to retrieve the filename including drive and path (AConfigBase::getFullFilename), the pure filename with a leading '\' (AConfigBase::getPureFilename) and only the path including the drive of the ini file (AConfigBase::getPathname).
parameter to be able to build the complete filename. It is passed by
AConfigFile and AIniFile during the initialization of these classes.


<p>The destructor is declared as pure virtual to make AConfigBase an
All other methods are declared as private. They are called by the constructor to build the name of the file. They can not be used from outside because the filename of an ini file object must not change during its lifetime.
abstract class.
<code>
 
<p>The three public methods are to retrieve the filename including drive
and path (AConfigBase::getFullFilename), the pure filename with a leading
'\' (AConfigBase::getPureFilename) and only the path including the drive
of the ini file (AConfigBase::getPathname).
 
<p>All other methods are declared as private. They are called by the
constructor to build the name of the file. They can not be used from
outside because the filename of an ini file object must not change during
its lifetime.
 
<pre><small>
   //**********************************************************
   //**********************************************************
   // AConfigBase :: AConfigBase - Constructor of AConfigBase *
   // AConfigBase :: AConfigBase - Constructor of AConfigBase *
   //**********************************************************
   //**********************************************************
 
   AConfigBase :: AConfigBase (const char *  pszSuffix)
   AConfigBase :: AConfigBase (const char *  pszSuffix)
                               : m_strFullFilename (),
                               : m_strFullFilename (),
Line 188: Line 119:
     AFile file;
     AFile file;
     IString strFile;
     IString strFile;
 
     filenameFromAppDir (pszSuffix);
     filenameFromAppDir (pszSuffix);
                   // build the filename from the app directory
                   // build the filename from the app directory
 
     if(tryOpenFile (file, strFile))
     if(tryOpenFile (file, strFile))
                 // the ini data can be saved in the app
                 // the ini data can be saved in the app
                 // directory
                 // directory
       return;
       return;
 
     if(!filenameFromIniDir ())          // == NO_ERROR
     if(!filenameFromIniDir ())          // == NO_ERROR
       {
       {
Line 204: Line 135:
           return;
           return;
       }
       }
 
     switch((m_ret = filenameFromCurrDir ()))
     switch((m_ret = filenameFromCurrDir ()))
                                 // build the filename from the
                                 // build the filename from the
Line 224: Line 155:
                         // something unexpected has gone wrong
                         // something unexpected has gone wrong
           }
           }
 
     filenameFromSystemDir ();
     filenameFromSystemDir ();
               // build the filename from the system directory
               // build the filename from the system directory
 
     if(!tryOpenFile (file, strFile))
     if(!tryOpenFile (file, strFile))
                         // the ini data can't be saved in the
                         // the ini data can't be saved in the
Line 235: Line 166:
               // something unexpected has gone wrong
               // something unexpected has gone wrong
   }
   }
</small></pre>
</code>
''Figure 3: Constructor of AConfigBase.''


<font SIZE=2>
The constructor tries to build the filename of the ini file using the sequence described above.
Figure 3: Constructor of AConfigBase.


</font>
The AConfigBase::filenameFrom... methods use the different API calls to determinate the values they need. AConfigBase::filenameFromAppDir has also the task of setting the pure filename. That's because DosQueryModuleName is the way to get the filename's prefix. Since the app directory is the first place where to place the ini file, there's no extra method needed to build the pure filename.


<p>The constructor tries to build the filename of the ini file using the
These methods return different values. AConfigBase::filenameFromAppDir and AConfigBase::filenameFromSystemDir return a reference to the AConfigBase object. If a problem occurs in one of these methods, this will be a problem that cannot be handled correctly by the ini object, so an IAccessError is thrown.
sequence described above.


<p>The AConfigBase::filenameFrom... methods use the different API calls to
The error that may be detected by AConfigBase::filenameFromAppDir is that the module name cannot be queried by the API function DosQueryModuleName. The error that may arise in AConfigBase::filenameFromSystemDir is that the system path can't be queried by DosQuerySysInfo. The exceptions are not caught by the constructor. You have catch them yourself. The source of the sample application shows one way how to do this.
determinate the values they need. AConfigBase::filenameFromAppDir has also
the task of setting the pure filename. That's because DosQueryModuleName
is the way to get the filename's prefix. Since the app directory is the
first place where to place the ini file, there's no extra method needed to
build the pure filename.


<p>These methods return different values. AConfigBase::filenameFromAppDir
AConfigBase::filenameFromIniDir and AConfigBase::filenameFromCurrDir return the value received by the API call to query the environment variable USER_INI (DosScanEnv) and to query the current directory (DosQueryCurrentDir) respectively.
and AConfigBase::filenameFromSystemDir return a reference to the
AConfigBase object. If a problem occurs in one of these methods, this will
be a problem that cannot be handled correctly by the ini object, so an
IAccessError is thrown.


<br>The error that may be detected by AConfigBase::filenameFromAppDir is
If AConfigBase::filenameFromIniDir does not return NO_ERROR, the value of USER_INI cannot be determined. I can't tell if the operating system misses this entry, but here that is not too bad. So we can continue trying to build the filename from the current directory.
that the module name cannot be queried by the API function
DosQueryModuleName. The error that may arise in
AConfigBase::filenameFromSystemDir is that the system path can't be
queried by DosQuerySysInfo. The exceptions are not caught by the
constructor. You have catch them yourself. The source of the sample
application shows one way how to do this.


<p>AConfigBase::filenameFromIniDir and AConfigBase::filenameFromCurrDir
The constructor can handle three return values from AConfigBase::filenameFromCurrDir. NO_ERROR means the current directory was used to build the filename and we can try to put the ini file there.
return the value received by the API call to query the environment
variable USER_INI (DosScanEnv) and to query the current directory
(DosQueryCurrentDir) respectively.


<br>If AConfigBase::filenameFromIniDir does not return NO_ERROR, the value
ERROR_NOT_DOS_DISK and ERROR_DRIVE_LOCKED mean we have to try to build the filename from the system directory. Any other return value means that something has gone wrong. In this case an IAccessError is thrown. For a complete list of return values please refer to the OS/2 API manual.
of USER_INI cannot be determined. I can't tell if the operating system
misses this entry, but here that is not too bad. So we can continue trying
to build the filename from the current directory.


<br>The constructor can handle three return values from
If none of the four directories can be used to store the ini file (also ACongfigBase::tryOpenFile using the system directory returns false), an IAccessError is thrown.
AConfigBase::filenameFromCurrDir. NO_ERROR means the current directory was
<code>
used to build the filename and we can try to put the ini file there.
ERROR_NOT_DOS_DISK and ERROR_DRIVE_LOCKED mean we have to try to build the
filename from the system directory. Any other return value means that
something has gone wrong. In this case an IAccessError is thrown. For a
complete list of return values please refer to the OS/2 API manual.
 
<p>If none of the four directories can be used to store the ini file (also
ACongfigBase::tryOpenFile using the system directory returns false), an
IAccessError is thrown.
 
<pre><small>
   //**********************************************************
   //**********************************************************
   // AConfigBase :: tryOpenFile - tries to open the ini file *
   // AConfigBase :: tryOpenFile - tries to open the ini file *
Line 294: Line 192:
   // throws IAccessError                                    *
   // throws IAccessError                                    *
   //**********************************************************
   //**********************************************************
 
   Boolean AConfigBase :: tryOpenFile (AFile& rFile,
   Boolean AConfigBase :: tryOpenFile (AFile& rFile,
                                       IString& rstrFile)
                                       IString& rstrFile)
 
   {
   {
     rstrFile = m_strPathname + "\\~tryopen.tmp";
     rstrFile = m_strPathname + "\\~tryopen.tmp";
 
     switch((m_ret = rFile. open (rstrFile, 0L, FILE_NORMAL,
     switch((m_ret = rFile. open (rstrFile, 0L, FILE_NORMAL,
                               OPEN_ACTION_CREATE_IF_NEW
                               OPEN_ACTION_CREATE_IF_NEW
Line 329: Line 227:
                 }
                 }
           }
           }
 
     return (false);
     return (false);
   }
   }
</small></pre>
</code>
''Figure 4: AConfigBase::tryOpenFile.''


<font SIZE=2>
Figure 4 shows the key method to check out if the ini file can be placed inside of the desired directory. I'm using my class AFile (please refer to 'A Progress-indicating Status Line in C++' [3]) for this purpose.
Figure 4: AConfigBase::tryOpenFile.
</font>


<p>Figure 4 shows the key method to check out if the ini file can be
Of course you can use DosOpen instead. We need here OPEN_ACTION_CREATE_IF_NEW | OPEN_ACTION_REPLACE_IF_EXISTS as open flags. Normally, the testfile ~tryopen.tmp should not exist. So we have to use OPEN_ACTION_CREATE_IF_NEW. In the really improbable case that this file exists, we have to try to recreate it to see if we have write permissions on the drive. Therefore we also need OPEN_ACTION_REPLACE_IF_EXISTS.
placed inside of the desired directory. I'm using my class AFile (please
 
refer to 'A Progress-indicating Status Line in C++' [3]) for this purpose.
The default open modes of AFile::open are OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYREADWRITE. These must be expanded by OPEN_FLAGS_FAIL_ON_ERROR because the media I/O errors should not be handled by the system critical-error handler (the user gets a system message on the screen) but be reported as a return value. Now the parameters are what we need to see if the ini file can be used in the way we want it. Because we use a temporary file for testing it is guaranteed that we do not destroy any previously saved data. If AFile::open returns NO_ERROR, we can use the directory and the method returns true. If AFile::open returns ERROR_WRITE_PROTECT or ERROR_OPEN_FAILED, then we should try the next directory and AConfigBase::tryOpenFile returns false.
Of course you can use DosOpen instead. We need here
OPEN_ACTION_CREATE_IF_NEW | OPEN_ACTION_REPLACE_IF_EXISTS as open flags.
Normally, the testfile ~tryopen.tmp should not exist. So we have to use
OPEN_ACTION_CREATE_IF_NEW. In the really improbable case that this file
exists, we have to try to recreate it to see if we have write permissions
on the drive. Therefore we also need OPEN_ACTION_REPLACE_IF_EXISTS.


<p>The default open modes of AFile::open are OPEN_ACCESS_READWRITE |
OPEN_SHARE_DENYREADWRITE. These must be expanded by
OPEN_FLAGS_FAIL_ON_ERROR because the media I/O errors should not be
handled by the system critical-error handler (the user gets a system
message on the screen) but be reported as an return value. Now the
parameters are what we need to see if the ini file can be used in the way
we want it. Because we use a temporary file for testing it is guaranteed
that we do not destroy any previously saved data. If AFile::open returns
NO_ERROR, we can use the directory and the method returns true. If
AFile::open returns ERROR_WRITE_PROTECT or ERROR_OPEN_FAILED, then we
should try the next directory and AConfigBase::tryOpenFile returns false.
Any other return values causes an exception of type IAccessError.
Any other return values causes an exception of type IAccessError.
 
<h2>AConfigFile - Do It Non-Conform</h2>
== AConfigFile - Do It Non-Conform ==
 
The class AConfigFile is defined in cfgfile.h and cfgfile.c.
<p>The class AConfigFile is defined in cfgfile.h and cfgfile.c.
<code>
 
<pre><small>
   template <class T> class AConfigFile : public AConfigBase,
   template <class T> class AConfigFile : public AConfigBase,
                                         public AFile
                                         public AFile
   {
   {
     public:
     public:
       AConfigFile ();
       AConfigFile ();
       virtual ~AConfigFile () = 0;
       virtual ~AConfigFile () = 0;
 
       T * getConfigData () { return (&m_ConfigData); }
       T * getConfigData () { return (&m_ConfigData); }
 
   APIRET open (const ULONG ulFileAttribute = FILE_NORMAL,
   APIRET open (const ULONG ulFileAttribute = FILE_NORMAL,
             const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE,
             const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE,
Line 387: Line 264:
                     // open the ini file
                     // open the ini file
     }
     }
 
   APIRET read (const ULONG ulFileAttribute = FILE_NORMAL,
   APIRET read (const ULONG ulFileAttribute = FILE_NORMAL,
               const ULONG ulOpenFlag = FILE_OPEN,
               const ULONG ulOpenFlag = FILE_OPEN,
Line 393: Line 270:
                                       | OPEN_SHARE_DENYWRITE,
                                       | OPEN_SHARE_DENYWRITE,
               const PEAOP2 pEABuf = 0L);
               const PEAOP2 pEABuf = 0L);
 
   APIRET write (const ULONG ulFileAttribute = FILE_NORMAL,
   APIRET write (const ULONG ulFileAttribute = FILE_NORMAL,
             const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE,
             const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE,
Line 399: Line 276:
                                   | OPEN_SHARE_DENYREADWRITE,
                                   | OPEN_SHARE_DENYREADWRITE,
             const PEAOP2 pEABuf = 0L);
             const PEAOP2 pEABuf = 0L);
 
     private:
     private:
       T m_ConfigData;
       T m_ConfigData;
   };
   };
</small></pre>
</code>
''Figure 5: Declaration of AConfigFile.''


<font SIZE=2>
As you can see in figure 5, AConfigFile is a template class derived from AConfigBase. This gives you the ability to use it for any configuration structure needed. It is also derived from AFile, since the configuration data is stored in a simple file.
Figure 5: Declaration of AConfigFile.
</font>


<p>As you can see in figure 5, AConfigFile is a template class derived
There is nothing mysterious about this class. AFile's read and write methods are overridden because AConfigFile always keeps the file closed.
from AConfigBase. This gives you the ability to use it for any
AConfigFile::read and AConfigFile::write checks if the file is open, open it if not, and close it again after reading/writing. When you use the makefiles included with the sample code, you will retrieve a warning that AConfigFile::open hides both AFile::open methods, AFile::read and AFile::write. That's because I want to avoid the use of a filename different from the one built by AConfigBase during initialization and the use of the AFile::methods for reading and writing.
configuration structure needed. It is also derived from AFile, since the
configuration data is stored in a simple file.


<p>There is nothing mysterious about this class. AFile's read and write
Since the configuration data is a private member, you have to access it by AConfigFile::getConfigData, which returns a pointer to the structure.
methods are overridden because AConfigFile always keeps the file closed.
AConfigFile::read and AConfigFile::write checks if the file is open, open
it if not, and close it again after reading/writing. When you use the
makefiles included with the sample code, you will retrieve a warning that
AConfigFile::open hides both AFile::open methods, AFile::read and
AFile::write. That's because I want to avoid the use of a filename
different from the one built by AConfigBase during initialization and the
use of the AFile::methods for reading and writing.


<p>Since the configuration data is a private member, you have to access it
== IBM Programming Guide Conform - AIniFile ==
by AConfigFile::getConfigData, which returns a pointer to the structure.
The class AIniFile is defined in inifile.hpp and inifile.cpp.
 
<code>
<h2>IBM Programming Guide Conform - AIniFile</h2>
 
<p>The class AIniFile is defined in inifile.hpp and inifile.cpp.
 
<pre><small>
   class AIniFile : public AConfigBase,
   class AIniFile : public AConfigBase,
                   public IProfile
                   public IProfile
 
   {
   {
     public:
     public:
Line 440: Line 301:
       virtual ~AIniFile () = 0;
       virtual ~AIniFile () = 0;
   };
   };
</small></pre>
</code>
''Figure 6: Declaration of AIniFile.''


<font SIZE=2>
As you can see in figure 6, AIniFile only contains a constructor and a (empty) destructor. All functionality needed here is inherited from the base classes AConfigBase and IProfile.
Figure 6: Declaration of AIniFile.
</font>


<p>As you can see in figure 6, AIniFile only contains a constructor and a
One really important thing is the sequence of the declaration of the base classes. AConfigBase must be declared before IProfile. Have a look at the constructor code in figure 7:
(empty) destructor. All functionality needed here is inherited from the
<code>
base classes AConfigBase and IProfile.
 
<p>One really important thing is the sequence of the declaration of the
base classes. AConfigBase must be declared before IProfile. Have a look at
the constructor code in figure 7:
 
<pre><small>
   //**********************************************************
   //**********************************************************
   // AIniFile :: AIniFile - Constructor of AIniFile          *
   // AIniFile :: AIniFile - Constructor of AIniFile          *
   //**********************************************************
   //**********************************************************
 
   AIniFile :: AIniFile ()
   AIniFile :: AIniFile ()
               : AConfigBase ("INI"),
               : AConfigBase ("INI"),
                 IProfile (getFullFilename ())
                 IProfile (getFullFilename ())
 
   {
   {
     IString strApplicationName (getPureFilename ());
     IString strApplicationName (getPureFilename ());
 
     strApplicationName. remove (0, 1);
     strApplicationName. remove (0, 1);
                                     // remove the leading '\'
                                     // remove the leading '\'
 
     strApplicationName. remove (strApplicationName. lastIndexOf ('.'));
     strApplicationName. remove (strApplicationName. lastIndexOf ('.'));
     // remove the suffix '.ini'
     // remove the suffix '.ini'
 
     setDefaultApplicationName (strApplicationName);
     setDefaultApplicationName (strApplicationName);
                                             // set the default
                                             // set the default
                                             // application name
                                             // application name
   }
   }
</small></pre>
</code>
 
''Figure 7: Constructor of AIniFile.''
<font SIZE=2>
Figure 7: Constructor of AIniFile.
</font>
 
<p>The constructor of IProfile needs the name of the initialization file
as an argument. This filename is part of the base class AConfigBase and is
built during the initialization of AConfigBase. So AConfigBase must be
initialized before IProfile. The sequence, in which the base classes are
initialized, is defined by the declaration order, not by the order in the
initialization list of the constructor of the derived class, here AIniFile
(see [4]).
 
<p>And because there is always just one application per initialization
file by definition, we can set the default application name of IProfile
during the initialization of AIniFile. It can be taken from the pure
filename of AConfigBase. Only the leading backslash and the suffix have to
be removed.
 


<h2>Handle The Configuration Data - ACfgConfig</h2>
The constructor of IProfile needs the name of the initialization file as an argument. This filename is part of the base class AConfigBase and is built during the initialization of AConfigBase. So AConfigBase must be initialized before IProfile. The sequence, in which the base classes are initialized, is defined by the declaration order, not by the order in the initialization list of the constructor of the derived class, here AIniFile (see [4]).


<p>The class ACfgConfig is defined in cfgcfg.hpp.
And because there is always just one application per initialization file by definition, we can set the default application name of IProfile during the initialization of AIniFile. It can be taken from the pure filename of AConfigBase. Only the leading backslash and the suffix have to be removed.


<pre><small>
== Handle The Configuration Data - ACfgConfig ==
The class ACfgConfig is defined in cfgcfg.hpp.
<code>
   typedef struct _CFG_CONFIGURATION {
   typedef struct _CFG_CONFIGURATION {
       SIZEL sizeWindow;
       SIZEL sizeWindow;
       BOOL bSizeWindowSaved;
       BOOL bSizeWindowSaved;
   } CFG_CONFIGURATION;
   } CFG_CONFIGURATION;
 
   class ACfgConfig : public AConfigFile <CFG_CONFIGURATION>
   class ACfgConfig : public AConfigFile <CFG_CONFIGURATION>
 
   {
   {
     public:
     public:
       Boolean isSizeSaved ()          // was a size saved?
       Boolean isSizeSaved ()          // was a size saved?
             { return (getConfigData () -> bSizeWindowSaved); }
             { return (getConfigData () -> bSizeWindowSaved); }
 
       SIZEL getSize ()              // retrieve the size
       SIZEL getSize ()              // retrieve the size
             { return (getConfigData () -> sizeWindow); }
             { return (getConfigData () -> sizeWindow); }
 
       ACfgConfig& setSize (const SIZEL sSize)
       ACfgConfig& setSize (const SIZEL sSize)
                                               // set the size
                                               // set the size
Line 525: Line 362:
                 }
                 }
   };
   };
</small></pre>
</code>
 
''Figure 8: The configuration structure and the declaration of ACfgConfig.''
<font SIZE=2>
Figure 8: The configuration structure and the declaration of ACfgConfig.
</font>
 
<p>Figure 8 shows a very simple configuration file class. It is used by
the sample program to save the window's size when the program gets closed.
The position is saved by an initialization file to show you the use of
both ini file classes.
 
<p>As you can see, there are three methods on each (here the one and only)
configuration value. One to check if a value was saved at least, one to
retrieve and one to set the value. The method to set the configuration
parameter also has to care about the is-saved flag. Setting such a flag is
the easiest way to determinate whether a configuration file contains a
value or not. Of course you can also check if, in this sample, the size
has a height and a width. But what if a width and height of 0 is
permitted? How do you detect whether the value is newly initialized, or is
saved there? Using the flag protects you from such pain.


<h2>Manage The Initialization Data - AIniConfig</h2>
Figure 8 shows a very simple configuration file class. It is used by the sample program to save the window's size when the program gets closed. The position is saved by an initialization file to show you the use of both ini file classes.


<p>The class AIniConfig is defined in inicfg.hpp and inicfg.cpp.
As you can see, there are three methods on each (here the one and only) configuration value. One to check if a value was saved at least, one to retrieve and one to set the value. The method to set the configuration parameter also has to care about the is-saved flag. Setting such a flag is the easiest way to determinate whether a configuration file contains a value or not. Of course you can also check if, in this sample, the size has a height and a width. But what if a width and height of 0 is permitted? How do you detect whether the value is newly initialized, or is saved there? Using the flag protects you from such pain.


<pre><small>
== Manage The Initialization Data - AIniConfig ==
The class AIniConfig is defined in inicfg.hpp and inicfg.cpp.
<code>
   class AIniConfig : public AIniFile
   class AIniConfig : public AIniFile
 
   {
   {
     public:
     public:
Line 559: Line 380:
                         // profile?
                         // profile?
                 { return (containsKeyName (m_pszPosition)); }
                 { return (containsKeyName (m_pszPosition)); }
 
       POINTL getPosition () const;    // retrieve the position
       POINTL getPosition () const;    // retrieve the position
 
       AIniConfig& setPosition (const POINTL pPosition);
       AIniConfig& setPosition (const POINTL pPosition);
                                           // save the position
                                           // save the position
 
     private:
     private:
       static const char * const m_pszPosition;
       static const char * const m_pszPosition;
                                           // position key name
                                           // position key name
   };
   };
</small></pre>
</code>
''Figure 9: Declaration of AIniConfig.''


<font SIZE=2>
Looking at figure 9 you can see that using AIniFile makes it quite easier to determine whether an initialization value exists or not. You just have to call IProfile::containsKeyName with the desired key name as parameter. If the initialization file contains the key, true is returned, otherwise false. In contrast to ACfgConfig you do not need an additional flag in the ini file.
Figure 9: Declaration of AIniConfig.
<code>
</font>
 
<p>Looking at figure 9 you can see that using AIniFile makes it quite
easier to determine whether an initialization value exists or not. You
just have to call IProfile::containsKeyName with the desired key name as
parameter. If the initialization file contains the key, true is returned,
otherwise false. In contrast to ACfgConfig you do not need an additional
flag in the ini file.
 
<pre><small>
   // define the string for the position key
   // define the string for the position key
   const char * const AIniConfig :: m_pszPosition = "Position";
   const char * const AIniConfig :: m_pszPosition = "Position";
</small></pre>
</code>
''Figure 10: Definition of AIniConfig::m_pszPosition.''


<font SIZE=2>
As shown in Figure 9, this class has a private static const member variable called m_pszPosition. It is a pointer to the key name used to access the position element of the initialization file. Because the key name is the same for every instance of AIniConfig, the pointer to it can be shared by all AIniConfig objects. So it is declared as static. And to
Figure 10: Definition of AIniConfig::m_pszPosition.
</font>
 
<p>As shown in Figure 9, this class has a private static const member
variable called m_pszPosition. It is a pointer to the key name used to
access the position element of the initialization file. Because the key
name is the same for every instance of AIniConfig, the pointer to it can
be shared by all AIniConfig objects. So it is declared as static. And to
be sure it always points to the same string, it is declared as const too.
be sure it always points to the same string, it is declared as const too.


<p>Maybe you will ask why you should declare a pointer to each key name. I
Maybe you will ask why you should declare a pointer to each key name. I think using a variable instead of typing the key name itself makes it easier to maintain the program. When you decide to rename a key during the development process, you just have to change the string one time: at the declaration of the pointer. And the compiler will check if you typed the variable name the right way. When you mistype the key name, you have to find it yourself.
think using a variable instead of typing the key name itself makes it
<code>
easier to maintain the program. When you decide to rename a key during the
development process, you just have to change the string one time: at the
declaration of the pointer. And the compiler will check if you typed the
variable name the right way. When you mistype the key name, you have to
find it yourself.
 
<pre><small>
   //**********************************************************
   //**********************************************************
   // AIniConfig :: getPosition - return the position        *
   // AIniConfig :: getPosition - return the position        *
   //**********************************************************
   //**********************************************************
 
   POINTL AIniConfig :: getPosition () const
   POINTL AIniConfig :: getPosition () const
 
   {
   {
     POINTL pointRet = { 0, 0 };// initialize the return value;
     POINTL pointRet = { 0, 0 };// initialize the return value;
 
     if(!isPositionSaved ())
     if(!isPositionSaved ())
                     // was an element saved for the position?
                     // was an element saved for the position?
       return (pointRet);                        // no, return
       return (pointRet);                        // no, return
 
     // an element for the position exists, so retrieve it
     // an element for the position exists, so retrieve it
 
     IString strPosition (elementWithKey (m_pszPosition));
     IString strPosition (elementWithKey (m_pszPosition));
 
     memcpy (&pointRet, (char *) strPosition, sizeof (POINTL));
     memcpy (&pointRet, (char *) strPosition, sizeof (POINTL));
                                   // convert IString to POINTL
                                   // convert IString to POINTL
     return (pointRet);
     return (pointRet);
   }
   }
</small></pre>
</code>
 
''Figure 11: Retrieve a binary element of an initialization file.''
<font SIZE=2>
Figure 11: Retrieve a binary element of an initialization file.
</font>


<p>IProfile::elementWithKey always returns an IString. If the value itself
IProfile::elementWithKey always returns an IString. If the value itself isn't a string, you have to convert it to the desired data type. Figure 11 shows you how the saved window position of the sample application (binary data) is retrieved and converted. If the saved data is numeric, you can use one of IString's type conversation methods to get the desired type.
isn't a string, you have to convert it to the desired data type. Figure 11
<code>
shows you how the saved window position of the sample application (binary
data) is retrieved and converted. If the saved data is numeric, you can
use one of IString's type conversation methods to get the desired type.
 
<pre><small>
   //**********************************************************
   //**********************************************************
   // AIniConfig :: setPosition - set the position            *
   // AIniConfig :: setPosition - set the position            *
   //**********************************************************
   //**********************************************************
 
   AIniConfig& AIniConfig::setPosition (const POINTL pPosition)
   AIniConfig& AIniConfig::setPosition (const POINTL pPosition)
 
   {
   {
     IString strPosition (&pPosition, sizeof (POINTL), 0x0);
     IString strPosition (&pPosition, sizeof (POINTL), 0x0);
 
     addOrReplaceElementWithKey (m_pszPosition, strPosition);
     addOrReplaceElementWithKey (m_pszPosition, strPosition);
 
     return (*this);
     return (*this);
   }
   }
</small></pre>
</code>
 
''Figure 12: Save a binary element of an initialization file.''
<font SIZE=2>
Figure 12: Save a binary element of an initialization file.
</font>
 
<p>Because IProfile::addOrReplaceElementWithKey only accepts key values of
type long or IString, you have to convert binary data to IString before
saving. This can be done by one of IString's constructors, which accepts a
void pointer to a buffer, the buffer's size and a pad character as
parameters. AIniConfig::setPosition uses this way to save the window's
position of the sample application. After converting the POINTL structure
to an IString, IProfile::addOrReplaceElementWithKey can be used to save
the data.


<h2>Which File Shall I Use?</h2>
Because IProfile::addOrReplaceElementWithKey only accepts key values of type long or IString, you have to convert binary data to IString before saving. This can be done by one of IString's constructors, which accepts a void pointer to a buffer, the buffer's size and a pad character as parameters. AIniConfig::setPosition uses this way to save the window's
position of the sample application. After converting the POINTL structure to an IString, IProfile::addOrReplaceElementWithKey can be used to save the data.


<p>As you can see when you're looking at the sample code, using the
== Which File Shall I Use? ==
initialization file ends up in more coding and marginal slower performance
As you can see when you're looking at the sample code, using the initialization file ends up in more coding and marginal slower performance because of the many type conversations of binary data. But having a good concept of how to organize the keys and writing an easy to use
because of the many type conversations of binary data. But having a good
initialization class can make it really easy for others to use the data and easy for you to maintain it. So play around with the sample to find out which you like most.
concept of how to organize the keys and writing an easy to use
initialization class can make it really easy for others to use the data
and easy for you to maintain it. So play around with the sample to find
out which you like most.


<h2>The Sample Application</h2>
== The Sample Application ==
The sample application initest shows you the use of both ways to handle your ini data. Of course this is not a typical sample, since you will almost never use both ini file classes in one application. But here it is done for demonstration purpose.


<p>The sample application initest shows you the use of both ways to handle
When you run the sample, be sure to close it using the F3 key or the menu 'File' 'Exit'. Any other way to end it doesn't saves the current position and size of the window. The position is saved in initest.ini, the size in initest.cfg.
your ini data. Of course this is not a typical sample, since you will
almost never use both ini file classes in one application. But here it is
done for demonstration purpose.


<p>When you run the sample, be sure to close it using the F3 key or the
When you build the sample application, you can use the makefiles included in the source zip. There is one makefile for IBM's C Set++ (initest.mac) and one for IBM's VisualAge C++ (initest.mav). If you decide to use your own makefile, make sure all switches especially for the use of templates are set correctly.
menu 'File' 'Exit'. Any other way to end it doesn't saves the current
position and size of the window. The position is saved in initest.ini, the
size in initest.cfg.


<p>When you build the sample application, you can use the makefiles
# IBM, Programming Guide Volume II, e.g. Developer Connection Disk 2, Directory Docs, pmv2base.inf, 1996
included in the source zip. There is one makefile for IBM's CSet++
# Harald Wilhelm, Register me!, OS/2 Inside, 5/97, 1997, p. 65-69
(initest.mac) and one for IBM's Visual Age for C++ (initest.mav). If you
# Stefan Ruck, [[A Progress-indicating Status Line in C++ (Part 2)]], EDM/2 Volume 4 Issue 6, 1996
decide to use your own makefile, make sure all switches especially for the
# Bjarne Stroustrup, The C++ Programming Language, Second Edition, 1995, p. 580
use of templates are set correctly.


<pre><small>
[[Category:C++ Articles]]
[1] IBM, Programming Guide Volume II, e.g. Developer Connection Disk 2,
    Directory Docs, pmv2base.inf, 1996
[2] Harald Wilhelm, Register me!, OS/2 Inside, 5/97, 1997, p. 65-69
[3] Stefan Ruck, A Progress-indicating Status Line in C++ (Part 2),
    EDM/2 Volume 4 Issue 6, 1996
[4] Bjarne Stroustrup, The C++ Programming Language, Second Edition,
    1995, p. 580f
</small></pre>

Latest revision as of 21:59, 10 October 2022

Written by Stefan Ruck

[NOTE: Here is a link to a zip of the http://www.edm2.com/0509/confsrc.zip source code for this article. Ed.]

Introduction

Almost every program needs some configuration data. But where to store it? How to save and retrieve the data?

In this article I will show you two different ways to manage your configuration data. One describes a template class which gives you the ability to store your data in a self-defined structure. The other gives you a class which is derived from IProfile. This class conforms to IBM's programming guide.

When you look at the description of configuration files in IBM's programming guide vol. II [1], the guide is always talking about initialization files. I will make a distinction between configuration and initialization files. When I use 'configuration files' I mean those files which use a self-defined structure to store the data. 'Initialization files' are the files which store the data conforming to the programming guide using the IProfile class or my derived AIniFile class. I will use 'ini file' for the global meaning.

Where To Place The Data?

What I hadn't found in the programming guide is a specification about the name of the file and its location. Also IProfile accepts any filename passed to its constructor.

Harald Wilhelm gave the base definition in his article 'Register me!' [2], which I expanded a little bit. He said that the filename should be the same as the program name except the filename suffix, which should almost always be 'ini'. The sequence for where to place the file was defined as follows:

  • The directory of the main program. If this is write protected,
  • The current directory. If this is write protected too,
  • The system directory (\os2).

I think the second place to look for is the directory where the user ini file (os2.ini) is located. This is set in the config.sys by the environment variable USER_INI. Then follows the current and at last the system directory.

Setting the filename suffix I distinguish between the configuration file (suffix 'cfg') and the initialization file (suffix 'ini').

I think you should NEVER use the os2.ini file to store your initialization data. There are several reasons why not. The first is a simple question: Have you ever corrupted the registry of your Win95 or NT system? Another point is when you save the configuration in a separate file, it's easy for the user to reconfigure his system without losing his application configurations. And thinking about it a little bit I guess you'll find a few more good reasons.

Some Restrictions

As defined above, each application should have its own ini file. This concept is strictly realized in the classes I've written. It is a reduction against the IProfile class and the OS/2 API which gives you the ability to store data for different applications in the same initialization file. The unique data key in these files is always the combination application / user key. You can't pass a filename to the classes I've written. The filename is set by the object itself at runtime. So you can never read the ini file of another application or a global one.

The Class Hierarchy:

                         AConfigBase
                      (abstract class)
                             |
              -----------------------------
              |                           |
          AConfigFile                  AIniFile
       (abstract class)            (abstract class)
              |                           |
          ACfgConfig                  AIniConfig
 (application specific class)  (application specific class)

Figure 1: The ini file class hierarchy.

AConfigBase is the base class of AConfigFile, which handles configuration files, and AIniFile, which covers initialization files. AConfigBase handles building the filename, which is similar for both AConfigFile and AIniFile.

AConfigFile and AIniFile are abstract classes as AConfigBase. This means you cannot create an object of these classes. You have to derive an application specific class from AConfigFile or AIniFile for use.

Why is that? In my opinion AConfigFile and AIniFile are still incomplete. A complete ini class needs an interface to access the different configuration parameters. An example for complete ini classes are ACfgConfig and AIniConfig (see below).

The Base For All Ini Files: AConfigBase

The class AConfigBase is defined in cfgbase.hpp and cfgbase.cpp.

 class AConfigBase:public IBase
 {
   public:
     AConfigBase (const char* pszSuffix);

     virtual ~AConfigBase () = 0;

     const IString& getFullFilename ()
                   { return (m_strFullFilename); }
     const IString& getPureFilename ()
                   { return (m_strPureFilename); }
     const IString& getPathname ()
                   { return (m_strPathname); }

  private:
    AConfigBase& filenameFromAppDir (const char * pszSuffix);
                         // build filename from app directory
                  // throws IAccessError

     APIRET filenameFromIniDir ();
                         // build filename from ini directory

     APIRET filenameFromCurrDir ();
                     // build filename from current directory

     AConfigBase& filenameFromSystemDir ();
                               // build filename from system
                               // directory
                  // throws IAccessError

     AConfigBase& buildPathname ();
                // build the pathname from m_strFullFilename

     Boolean tryOpenFile (AFile& rFile, IString& rstrFile);
             // throws IAccessError

     IString m_strFullFilename,  // filename including path
             m_strPureFilename,
             // filename without path, but with leading '\'
             m_strPathname;      // only the path

     APIRET m_ret;
                // keeps the return value of the API calls
 };

Figure 2: Declaration of AConfigBase.

The constructor of AConfigBase needs the suffix of the ini file as a parameter to be able to build the complete filename. It is passed by AConfigFile and AIniFile during the initialization of these classes.

The destructor is declared as pure virtual to make AConfigBase an abstract class.

The three public methods are to retrieve the filename including drive and path (AConfigBase::getFullFilename), the pure filename with a leading '\' (AConfigBase::getPureFilename) and only the path including the drive of the ini file (AConfigBase::getPathname).

All other methods are declared as private. They are called by the constructor to build the name of the file. They can not be used from outside because the filename of an ini file object must not change during its lifetime.

 //**********************************************************
 // AConfigBase :: AConfigBase - Constructor of AConfigBase *
 //**********************************************************

 AConfigBase :: AConfigBase (const char *  pszSuffix)
                             : m_strFullFilename (),
                               m_strPureFilename (),
                               m_strPathname (),
                               m_ret (NO_ERROR)
 {
   AFile file;
   IString strFile;

   filenameFromAppDir (pszSuffix);
                 // build the filename from the app directory

   if(tryOpenFile (file, strFile))
                // the ini data can be saved in the app
                // directory
      return;

   if(!filenameFromIniDir ())          // == NO_ERROR
      {
       if(tryOpenFile (file, strFile))
                      // the ini data can be saved in the ini
                      // directory
          return;
      }

   switch((m_ret = filenameFromCurrDir ()))
                               // build the filename from the
                               // current directory
          {
           case NO_ERROR:
                if(tryOpenFile (file, strFile))
                              // the ini data can be saved in
                              // the current directory
                   return;
                break;
           case ERROR_NOT_DOS_DISK:
           case ERROR_DRIVE_LOCKED:
                break;
           default:
                ITHROW (IAccessError ("DosQueryModuleName",
                                      m_ret,
                               IException :: unrecoverable));
                       // something unexpected has gone wrong
          }

   filenameFromSystemDir ();
              // build the filename from the system directory

   if(!tryOpenFile (file, strFile))
                        // the ini data can't be saved in the
                        // system directory
      ITHROW (IAccessError("DosOpen", m_ret,
                           IException :: unrecoverable));
              // something unexpected has gone wrong
 }

Figure 3: Constructor of AConfigBase.

The constructor tries to build the filename of the ini file using the sequence described above.

The AConfigBase::filenameFrom... methods use the different API calls to determinate the values they need. AConfigBase::filenameFromAppDir has also the task of setting the pure filename. That's because DosQueryModuleName is the way to get the filename's prefix. Since the app directory is the first place where to place the ini file, there's no extra method needed to build the pure filename.

These methods return different values. AConfigBase::filenameFromAppDir and AConfigBase::filenameFromSystemDir return a reference to the AConfigBase object. If a problem occurs in one of these methods, this will be a problem that cannot be handled correctly by the ini object, so an IAccessError is thrown.

The error that may be detected by AConfigBase::filenameFromAppDir is that the module name cannot be queried by the API function DosQueryModuleName. The error that may arise in AConfigBase::filenameFromSystemDir is that the system path can't be queried by DosQuerySysInfo. The exceptions are not caught by the constructor. You have catch them yourself. The source of the sample application shows one way how to do this.

AConfigBase::filenameFromIniDir and AConfigBase::filenameFromCurrDir return the value received by the API call to query the environment variable USER_INI (DosScanEnv) and to query the current directory (DosQueryCurrentDir) respectively.

If AConfigBase::filenameFromIniDir does not return NO_ERROR, the value of USER_INI cannot be determined. I can't tell if the operating system misses this entry, but here that is not too bad. So we can continue trying to build the filename from the current directory.

The constructor can handle three return values from AConfigBase::filenameFromCurrDir. NO_ERROR means the current directory was used to build the filename and we can try to put the ini file there.

ERROR_NOT_DOS_DISK and ERROR_DRIVE_LOCKED mean we have to try to build the filename from the system directory. Any other return value means that something has gone wrong. In this case an IAccessError is thrown. For a complete list of return values please refer to the OS/2 API manual.

If none of the four directories can be used to store the ini file (also ACongfigBase::tryOpenFile using the system directory returns false), an IAccessError is thrown.

 //**********************************************************
 // AConfigBase :: tryOpenFile - tries to open the ini file *
 //                                                         *
 // throws IAccessError                                     *
 //**********************************************************

 Boolean AConfigBase :: tryOpenFile (AFile& rFile,
                                     IString& rstrFile)

 {
   rstrFile = m_strPathname + "\\~tryopen.tmp";

   switch((m_ret = rFile. open (rstrFile, 0L, FILE_NORMAL,
                             OPEN_ACTION_CREATE_IF_NEW
                             | OPEN_ACTION_REPLACE_IF_EXISTS,
                             OPEN_FLAGS_FAIL_ON_ERROR
                             | OPEN_ACCESS_READWRITE
                             | OPEN_SHARE_DENYREADWRITE)))
          {
           case NO_ERROR:               // the filename is ok
                rFile. close ();
                if (DosDelete (rstrFile))
                                 // can't delete the testfile
                    ITHROW (IAccessError("DosDelete", m_ret,
                                 IException :: recoverable));
                return (true);
           case ERROR_WRITE_PROTECT:
           case ERROR_OPEN_FAILED:
                     // the ini data can't be saved using the
                     // filename
                return (false);
           default:
                {
                 IString error(rstrFile);
                 error += " DosOpen";
                 ITHROW (IAccessError(error, m_ret,
                         IException :: unrecoverable));
                       // something unexpected has gone wrong
                }
          }

   return (false);
 }

Figure 4: AConfigBase::tryOpenFile.

Figure 4 shows the key method to check out if the ini file can be placed inside of the desired directory. I'm using my class AFile (please refer to 'A Progress-indicating Status Line in C++' [3]) for this purpose.

Of course you can use DosOpen instead. We need here OPEN_ACTION_CREATE_IF_NEW | OPEN_ACTION_REPLACE_IF_EXISTS as open flags. Normally, the testfile ~tryopen.tmp should not exist. So we have to use OPEN_ACTION_CREATE_IF_NEW. In the really improbable case that this file exists, we have to try to recreate it to see if we have write permissions on the drive. Therefore we also need OPEN_ACTION_REPLACE_IF_EXISTS.

The default open modes of AFile::open are OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYREADWRITE. These must be expanded by OPEN_FLAGS_FAIL_ON_ERROR because the media I/O errors should not be handled by the system critical-error handler (the user gets a system message on the screen) but be reported as a return value. Now the parameters are what we need to see if the ini file can be used in the way we want it. Because we use a temporary file for testing it is guaranteed that we do not destroy any previously saved data. If AFile::open returns NO_ERROR, we can use the directory and the method returns true. If AFile::open returns ERROR_WRITE_PROTECT or ERROR_OPEN_FAILED, then we should try the next directory and AConfigBase::tryOpenFile returns false.

Any other return values causes an exception of type IAccessError.

AConfigFile - Do It Non-Conform

The class AConfigFile is defined in cfgfile.h and cfgfile.c.

 template <class T> class AConfigFile : public AConfigBase,
                                        public AFile
 {
   public:
     AConfigFile ();
     virtual ~AConfigFile () = 0;

     T * getConfigData () { return (&m_ConfigData); }

 APIRET open (const ULONG ulFileAttribute = FILE_NORMAL,
            const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE,
            const ULONG ulOpenMode = OPEN_ACCESS_READWRITE
                                  | OPEN_SHARE_DENYREADWRITE,
            const PEAOP2 pEABuf = 0L)
     {
      return (AFile :: open (getFullFilename (), sizeof (T),
                             ulFileAttribute,
                             ulOpenFlag, ulOpenMode, pEABuf);
                    // open the ini file
    }

 APIRET read (const ULONG ulFileAttribute = FILE_NORMAL,
             const ULONG ulOpenFlag = FILE_OPEN,
             const ULONG ulOpenMode = OPEN_ACCESS_READONLY
                                      | OPEN_SHARE_DENYWRITE,
             const PEAOP2 pEABuf = 0L);

 APIRET write (const ULONG ulFileAttribute = FILE_NORMAL,
            const ULONG ulOpenFlag = FILE_OPEN | FILE_CREATE,
            const ULONG ulOpenMode = OPEN_ACCESS_WRITEONLY
                                  | OPEN_SHARE_DENYREADWRITE,
            const PEAOP2 pEABuf = 0L);

   private:
     T m_ConfigData;
 };

Figure 5: Declaration of AConfigFile.

As you can see in figure 5, AConfigFile is a template class derived from AConfigBase. This gives you the ability to use it for any configuration structure needed. It is also derived from AFile, since the configuration data is stored in a simple file.

There is nothing mysterious about this class. AFile's read and write methods are overridden because AConfigFile always keeps the file closed. AConfigFile::read and AConfigFile::write checks if the file is open, open it if not, and close it again after reading/writing. When you use the makefiles included with the sample code, you will retrieve a warning that AConfigFile::open hides both AFile::open methods, AFile::read and AFile::write. That's because I want to avoid the use of a filename different from the one built by AConfigBase during initialization and the use of the AFile::methods for reading and writing.

Since the configuration data is a private member, you have to access it by AConfigFile::getConfigData, which returns a pointer to the structure.

IBM Programming Guide Conform - AIniFile

The class AIniFile is defined in inifile.hpp and inifile.cpp.

 class AIniFile : public AConfigBase,
                  public IProfile

 {
   public:
     AIniFile ();
     virtual ~AIniFile () = 0;
 };

Figure 6: Declaration of AIniFile.

As you can see in figure 6, AIniFile only contains a constructor and a (empty) destructor. All functionality needed here is inherited from the base classes AConfigBase and IProfile.

One really important thing is the sequence of the declaration of the base classes. AConfigBase must be declared before IProfile. Have a look at the constructor code in figure 7:

 //**********************************************************
 // AIniFile :: AIniFile - Constructor of AIniFile          *
 //**********************************************************

 AIniFile :: AIniFile ()
             : AConfigBase ("INI"),
               IProfile (getFullFilename ())

 {
   IString strApplicationName (getPureFilename ());

   strApplicationName. remove (0, 1);
                                    // remove the leading '\'

   strApplicationName. remove (strApplicationName. lastIndexOf ('.'));
   // remove the suffix '.ini'

   setDefaultApplicationName (strApplicationName);
                                           // set the default
                                           // application name
 }

Figure 7: Constructor of AIniFile.

The constructor of IProfile needs the name of the initialization file as an argument. This filename is part of the base class AConfigBase and is built during the initialization of AConfigBase. So AConfigBase must be initialized before IProfile. The sequence, in which the base classes are initialized, is defined by the declaration order, not by the order in the initialization list of the constructor of the derived class, here AIniFile (see [4]).

And because there is always just one application per initialization file by definition, we can set the default application name of IProfile during the initialization of AIniFile. It can be taken from the pure filename of AConfigBase. Only the leading backslash and the suffix have to be removed.

Handle The Configuration Data - ACfgConfig

The class ACfgConfig is defined in cfgcfg.hpp.

 typedef struct _CFG_CONFIGURATION {
     SIZEL sizeWindow;
     BOOL bSizeWindowSaved;
 } CFG_CONFIGURATION;

 class ACfgConfig : public AConfigFile <CFG_CONFIGURATION>

 {
   public:
     Boolean isSizeSaved ()          // was a size saved?
           { return (getConfigData () -> bSizeWindowSaved); }

     SIZEL getSize ()               // retrieve the size
           { return (getConfigData () -> sizeWindow); }

     ACfgConfig& setSize (const SIZEL sSize)
                                              // set the size
               {
                 getConfigData () -> sizeWindow = sSize;
                 getConfigData () -> bSizeWindowSaved = TRUE;
                 return (*this);
               }
 };

Figure 8: The configuration structure and the declaration of ACfgConfig.

Figure 8 shows a very simple configuration file class. It is used by the sample program to save the window's size when the program gets closed. The position is saved by an initialization file to show you the use of both ini file classes.

As you can see, there are three methods on each (here the one and only) configuration value. One to check if a value was saved at least, one to retrieve and one to set the value. The method to set the configuration parameter also has to care about the is-saved flag. Setting such a flag is the easiest way to determinate whether a configuration file contains a value or not. Of course you can also check if, in this sample, the size has a height and a width. But what if a width and height of 0 is permitted? How do you detect whether the value is newly initialized, or is saved there? Using the flag protects you from such pain.

Manage The Initialization Data - AIniConfig

The class AIniConfig is defined in inicfg.hpp and inicfg.cpp.

 class AIniConfig : public AIniFile

 {
   public:
     Boolean isPositionSaved () const
                        // is a value for the position in the
                        // profile?
               { return (containsKeyName (m_pszPosition)); }

     POINTL getPosition () const;    // retrieve the position

     AIniConfig& setPosition (const POINTL pPosition);
                                         // save the position

   private:
     static const char * const m_pszPosition;
                                         // position key name
 };

Figure 9: Declaration of AIniConfig.

Looking at figure 9 you can see that using AIniFile makes it quite easier to determine whether an initialization value exists or not. You just have to call IProfile::containsKeyName with the desired key name as parameter. If the initialization file contains the key, true is returned, otherwise false. In contrast to ACfgConfig you do not need an additional flag in the ini file.

 // define the string for the position key
 const char * const AIniConfig :: m_pszPosition = "Position";

Figure 10: Definition of AIniConfig::m_pszPosition.

As shown in Figure 9, this class has a private static const member variable called m_pszPosition. It is a pointer to the key name used to access the position element of the initialization file. Because the key name is the same for every instance of AIniConfig, the pointer to it can be shared by all AIniConfig objects. So it is declared as static. And to be sure it always points to the same string, it is declared as const too.

Maybe you will ask why you should declare a pointer to each key name. I think using a variable instead of typing the key name itself makes it easier to maintain the program. When you decide to rename a key during the development process, you just have to change the string one time: at the declaration of the pointer. And the compiler will check if you typed the variable name the right way. When you mistype the key name, you have to find it yourself.

 //**********************************************************
 // AIniConfig :: getPosition - return the position         *
 //**********************************************************

 POINTL AIniConfig :: getPosition () const

 {
   POINTL pointRet = { 0, 0 };// initialize the return value;

   if(!isPositionSaved ())
                    // was an element saved for the position?
      return (pointRet);                        // no, return

   // an element for the position exists, so retrieve it

   IString strPosition (elementWithKey (m_pszPosition));

   memcpy (&pointRet, (char *) strPosition, sizeof (POINTL));
                                 // convert IString to POINTL
   return (pointRet);
 }

Figure 11: Retrieve a binary element of an initialization file.

IProfile::elementWithKey always returns an IString. If the value itself isn't a string, you have to convert it to the desired data type. Figure 11 shows you how the saved window position of the sample application (binary data) is retrieved and converted. If the saved data is numeric, you can use one of IString's type conversation methods to get the desired type.

 //**********************************************************
 // AIniConfig :: setPosition - set the position            *
 //**********************************************************

 AIniConfig& AIniConfig::setPosition (const POINTL pPosition)

 {
   IString strPosition (&pPosition, sizeof (POINTL), 0x0);

   addOrReplaceElementWithKey (m_pszPosition, strPosition);

   return (*this);
 }

Figure 12: Save a binary element of an initialization file.

Because IProfile::addOrReplaceElementWithKey only accepts key values of type long or IString, you have to convert binary data to IString before saving. This can be done by one of IString's constructors, which accepts a void pointer to a buffer, the buffer's size and a pad character as parameters. AIniConfig::setPosition uses this way to save the window's position of the sample application. After converting the POINTL structure to an IString, IProfile::addOrReplaceElementWithKey can be used to save the data.

Which File Shall I Use?

As you can see when you're looking at the sample code, using the initialization file ends up in more coding and marginal slower performance because of the many type conversations of binary data. But having a good concept of how to organize the keys and writing an easy to use initialization class can make it really easy for others to use the data and easy for you to maintain it. So play around with the sample to find out which you like most.

The Sample Application

The sample application initest shows you the use of both ways to handle your ini data. Of course this is not a typical sample, since you will almost never use both ini file classes in one application. But here it is done for demonstration purpose.

When you run the sample, be sure to close it using the F3 key or the menu 'File' 'Exit'. Any other way to end it doesn't saves the current position and size of the window. The position is saved in initest.ini, the size in initest.cfg.

When you build the sample application, you can use the makefiles included in the source zip. There is one makefile for IBM's C Set++ (initest.mac) and one for IBM's VisualAge C++ (initest.mav). If you decide to use your own makefile, make sure all switches especially for the use of templates are set correctly.

  1. IBM, Programming Guide Volume II, e.g. Developer Connection Disk 2, Directory Docs, pmv2base.inf, 1996
  2. Harald Wilhelm, Register me!, OS/2 Inside, 5/97, 1997, p. 65-69
  3. Stefan Ruck, A Progress-indicating Status Line in C++ (Part 2), EDM/2 Volume 4 Issue 6, 1996
  4. Bjarne Stroustrup, The C++ Programming Language, Second Edition, 1995, p. 580