Manage Your Configuration Files and Data

From EDM2
Jump to: navigation, search

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 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.

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 CSet++ (initest.mac) and one for IBM's Visual Age for 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