Files, Streams and Stores

The ability to be able to save and retrieve data is a fundamental requirement for most application programmers. Symbian OS offers files, streams and stores to fulfil this need. A stream is basically the representation of an object as a sequence of bytes; read and write streams are responsible for reading in and writing out data. A store is a collection of these streams, with each stream having a unique ID. Files are supported through the RFile API, which allows you to create and open files and read and write binary descriptors. In addition, streams can also be externalized to a file, and using the streaming API for saving and recalling data is often the easier option.

Files

The use of asynchronous services has been discussed in some depth in this chapter, so you are now in a position to learn about one of the most important asynchronous service providers, the File Server.

In order to use any of the functionality outlined in this chapter, you will need to #include <f32file.h> . You will also need to add efsrv.lib to the LIBRARY line of your mmp file.


Symbian OS Filenames and Pathnames

Before considering the function of the File Server, it is worth taking a moment to describe the details of Symbian OS filenames and pathnames.

The filing system has been designed to operate in a very similar manner to the DOS/Windows file system with which most developers will already be familiar. Fully qualified filenames generally consist of up to four components :

  • The drive name : a single letter followed by a colon (":").

  • The path : starting from either the root directory (if preceded by "\") or the session's current directory, with each level of the folder hierarchy separated by a backslash ("\").

  • The filename.

  • The file extension, preceded by a dot (" . ")

The most important difference between DOS and Symbian OS is that no fully qualified filename (including drive name, path, filename and extension) can exceed 256 characters . That is to say, the fully qualified filename of any file must fit completely within a TFileName object.

In common with DOS (but unlike otherwise similar UNIX systems), the file system preserves case but does not differentiate case. In other words, " c:\Documents\FILE.ext " is the same as " C:\dOCUMENTS\FiLe.EXT " as far as the file system is concerned .

The following characters cannot be used anywhere in the file or path name: < > " / . Note that spaces can be used. Full details of the rules associated with pathnames and filenames can be found in the SDK documentation.

On most Symbian OS platforms (including Series 60), c: is the main system drive (flash filing system) where all user data and system files are stored, z: is the ROM drive, and removable drives are usually mapped to d: onward. (In the case of Series 60, d: is a nonpersistent (temporary) RAM drive and removable drives are mapped from e: onward.)

In spite of all that has been said above, Series 60 1.x does not usually expose the file system to the user; however, Series 2.x does partially expose it, providing a file manager application.

RFs API

The File Server is accessed via a handle of type RFs , and each RFs connected to the File Server demands a certain level of server-side and system resource, so their number should therefore be kept to a minimum. The Control Environment ( CONE ), which forms part of the application framework for UI applications, provides a permanent handle to the File Server to help reduce the number of connected File Server Sessions needed. To access it, invoke FsSession() on the iCoeEnv member of your View class. The Control Environment and other application framework components will be covered in more detail in Chapter 4.

Connect() and Close()

Alternatively, connecting to the File Server is simply a matter of invoking Connect() on a File Server handle (or, to give it the proper name: File Server session ):

 void CElementsEngine::ConstructL()    {    User::LeaveIfError(iFs.Connect());    } 

where iFs is simply an RFs member of CElementsEngine . While it really would be very easy to have each object that needs a session with the File Server create its own, for efficiency's sake a reference to the session is passed down to all objects that need it. For example:

 void CElementsEngine::LoadFromCsvFilesL()    {    iMetalsCsvFileLoader = CCsvFileLoader::NewL(iFs, *iElementList, *this,       KMetalsFileName); 

Before the File Server session is allowed to go out of scope or its owner deleted, it must be closed:

 CElementsEngine::~CElementsEngine()    {    delete iMetalsCsvFileLoader;    iFs.Close();    } 

If the File Server session is locally scoped, and there is the danger of a leave before it is closed, use CleanupClosePushL() to ensure it is closed properly, should a leave occur. CleanupStack::PopAndDestroy() can then be used to close it explicitly, once done.

Remember that none of other key functions available on a File Server can be called before the session has been connected. All of the following ones return error codes rather than leaving:

  • MkDir() and MkDirAll() : These functions simply take the path of a new directory, relative to the current session path, and create the new directory. For MkDir() , all of the preceding directories must already exist. For MkDirAll() , any ancestor directories of the new directory will also be created if they do not already exist.

  • RmDir() : Removes the directory specified as a descriptor. The directory must be empty and cannot be the root.

  • SessionPath() : Retrieves the current session path, effectively the "current directory."

  • SetSessionPath() : Takes a descriptor containing the path relative to the current session path to set as the new session path. It must not contain a filename; otherwise an error will result.

Fetching a Directory Listing Using GetDir()

The full function prototype for RFs::GetDir() is:

[View full width]
 
[View full width]
TInt GetDir(const TDesC& aName, TUint aEntryAttMask, TUint aEntrySortKey, CDir*& aEntryList) const;

aName is simply the name of the directory whose listing is required.

aEntryAttMask is a bitmask specifying the entries of interest. KEnTRyAttNormal will specify all entries except directories, hidden and system files, for instance.

aEntrySortKey specifies the sort order. The values are specified in TEntryKey ESortNone is the default.

Finally, aEntryList is a reference to a pointer to the directory listing. This pointer should have no memory allocated to it prior to calling, otherwise the memory will be orphaned. Ownership of the newly created CDir object is returned to the caller; it is the caller's responsibility to delete it.

Here is the definition of the CDir public API:

 class CDir : public CBase    { public:    IMPORT_C virtual ~CDir();    IMPORT_C TInt Count() const;    IMPORT_C const TEntry& operator[](TInt anIndex) const;    IMPORT_C TInt Sort(TUint aEntrySortKey);    }; 

The API is pretty self-explanatory. The operator[] can be used to iterate through the Count() enTRies, and return a TEnTRy reference. TEnTRy is defined like this:

 class TEntry    { public:    IMPORT_C TEntry();    IMPORT_C TEntry(const TEntry& aEntry);    IMPORT_C TEntry& operator=(const TEntry& aEntry);    IMPORT_C TBool IsReadOnly() const;    IMPORT_C TBool IsHidden() const;    IMPORT_C TBool IsSystem() const;    IMPORT_C TBool IsDir() const;    IMPORT_C TBool IsArchive() const;    inline const TUid& operator[](TInt anIndex) const;    inline TBool IsUidPresent(TUid aUid) const;    inline TBool IsTypeValid() const;    inline TUid MostDerivedUid() const; public:    TUint iAtt;    TInt iSize;    TTime iModified;    TUidType iType;    TBufC<KMaxFileName> iName;    }; 

This code snippet will return the name of the first (chronological) file in the specified directory:

 void GetFirstFileNameL(RFs& aFs, const TDesC& aFolderName,    TDes& aFirstFileName)    {    CDir* dir = NULL;    User::LeaveIfError(aFs.GetDir(aFolderName, KEntryAttNormal,       ESortByDate, dir));    if (dir->Count())       {       // Some files were found, get the name of the zeroth entry       aFirstFileName = (*dir)[0].iName;       }    else       {       // No files foundreturn zero-length descriptor       aFirstFileName.Zero();       }    delete dir;            // Since we have ownership    } 

RFile API

While the File Server session does provide a useful API, there are no functions for writing and reading to and from files.

In order to manipulate individual files, an RFile object is needed. This type of handle is known as a subsession, for reasons that will become clearer when Symbian OS Client/Server architecture is discussed later in this chapter.

Only a small number of API methods are discussed here. For full details, see the SDK documentation.

Opening and Closing

As with many other R -class objects, RFile must be opened before use. Unlike previously encountered objects, RFile 's open function takes a number of arguments:

 Open(RFs& aFs, const TDesC& aName, TUint aFileMode); 

The first argument is a File Server session (which must itself have been connected previously). Naturally, the name of the file to open needs to be specified. Finally, the mode must be given, from the enumeration TFileMode . Options include EFileRead and EFileWrite .

Here is part of the second-phase constructor of CCsvFileLoader , showing its iFile member being opened:

 void CCsvFileLoader::ConstructL(const TDesC& aFileName)    {    iFileName = aFileName;    User::LeaveIfError(iFile.Open(iFs, iFileName, EFileRead));    } 

Before being deleted or going out of scope, the RFile subsession must be closed:

 CCsvFileLoader::~CCsvFileLoader()    {    iFile.Close();    } 

Reading

There are numerous Read() overloads, all of which read binary (8-bit) data into a TDes8 -derived descriptor. Half of the overloads take a trequestStatus& , the other half do not. This means there are basically two types of Read() overload ”synchronous and asynchronous. In most cases, the asynchronous overload should be used from within an Active Object. Only use the synchronous overload in extreme circumstances (such as a quick test harness), since long file reads will block the thread, affecting responsiveness.

The simplest overload just takes a descriptor reference and reads in data from the start of the file to fill as much of the buffer as possible. The length of the descriptor is naturally set to the number of bytes actually read, which may be less than the maximum length of the descriptor if there is insufficient data in the file to fill it. Further overloads allow you to specify the offset from the beginning of the file and/or the number of bytes to read in.

In all asynchronous cases there is no return type, nor can the function leave. Rather than returning KErrEOF , the end of file condition is signaled by simply returning a zero-length descriptor.

Here is the FillReadBufferL() function of CCsvFileLoader again. It simply fills the buffer with as much data as it can from the file:

 void CCsvFileLoader::FillReadBufferL()    {    User::LeaveIfError(iFile.Seek(ESeekStart, iFilePos));    iFile.Read(iReadBuffer, iReadBuffer.MaxSize(), iStatus);    SetActive();    } 

The same functionality could be achieved by various other means, but all are variants of the idea shown above.

It is not necessary to check the return value of Seek() , even if seeking moves beyond the end of the file. It will be obvious, because iReadBuffer will be of zero size when you next enter RunL() .

Writing

Write() mirrors Read() precisely, writing data from a tdesC8 to a file. Overloads are provided to specify where, within the file, the data is to go, and how much data is to be written. Again, all variants are available as synchronous and asynchronous overloads ”asynchronous functions are always preferred.

All file operations use 8-bit descriptors. 16-bit descriptors cannot automatically be converted into 8-bit descriptors, but there are two approaches to achieve this explicitly ”one will preserve the length of the descriptor (number of characters), the other the size (number of bytes).

The first method preserves the size of the descriptor, but results in an 8-bit descriptor of double the length. This writes the 16-bit descriptor to file exactly as it appears in memory (endianness issues aside), as shown in Figure 3-8.

Figure 3-8. Preserving size when writing a 16-bit descriptor to an 8-bit descriptor.


In code, this is written using:

 TPtrC8 ptrC8((TUint8*)des16.Ptr(), des16.Size()); User::LeaveIfError(iFile.Write(ptrC8, iStatus)); 

The request to read it back in then looks like:

 delete iBufC8; iBufC8 = NULL; TInt size = User::LeaveIfError(iFile.Size()); iBufC8 = HBufC8::NewL(size); TPtr8 ptr8 = iBufC8->Des();      // To access modifiable API User::LeaveIfError(iFile.Read(ptr8, iStatus)); 

And, in the RunL() method:

 TPtr16 ptr16((TUint16*)iBufC8.Ptr(), iBufC8.Size() / 2); iObserver.Notify(ptr16); 

The observer should be sure to make a copy of the data, using Alloc () , since the data buffer is actually owned by the Active Object's iBufC8 in this case.

The second method preserves the length of the descriptor, effectively converting from Unicode to ASCII, as shown in Figure 3-9.

Figure 3-9. Preserving length when writing a 16-bit descriptor to an 8-bit descriptor.


Note that some data may be lost ” all Unicode values greater than 255 (decimal) will be converted to 1 ! So, if you are working with a non-Latin character set, then you should probably make use of the first method.

The following code will write the 16-bit descriptor to file, converting it to 8-bit first:

 delete iBufC8; iBufC8 = NULL; iBufC8 = HBufC8::NewL(des16.Length()); TPtr8 ptr8 = iBufC8->Des();      // To access modifiable API ptr8.Copy(des16);                // Convert 16-bit into 8-bit User::LeaveIfError(iFile.Write(ptr8, iStatus)); 

The request to read it back in then looks like:

 delete iBufC8; iBufC8 = NULL; TInt size = User::LeaveIfError(iFile.Size()); iBufC8 = HBufC8::NewL(size); TPtr8 ptr8 = iBufC8->Des();      // To access modifiable API User::LeaveIfError(iFile.Read(iBuf8, iStatus)); 

And, in the RunL() method:

 delete iBufC16; iBufC16 = NULL; iBufC16 = HBufC16::NewL(iBufC8.Length()); TPtr16 ptr16 = iBufC16->Des();   // To access modifiable API ptr16.Copy(iBufC8);              // Convert 8-bit to 16-bit iObserver.Notify(ptr16); 

Again, the observer will probably need to make its own copy of the data contained in the Active Object's iBufC16 (pointed to by the local ptr16 in the RunL() ).

Seeking

Moves the current read or write position to the given location within the file. The seek mode can be specified to move the seek position relative to the current location, relative to the start or end or to the absolute address for ROM based files.

 User::LeaveIfError(iFile.Seek(ESeekStart, iFilePos)); 

The value returned by the function is the current (start-relative) file position. To find the current file position, use:

 TInt currentPosition = file.Seek(ESeekCurrent, 0); 

Streams

As you will have gathered from the previous discussion on RFile::Read() and RFile::Write() , RFile is not the easiest way of reading data to and from a file. While a resourceful developer could doubtless find many exotic ways of writing complex data objects into an RFile , Symbian OS provides a far more elegant method: streams.

Symbian OS streams provide a simple manner of externalizing and internalizing data, whether it be to file store, memory or any other arbitrary I/O device. Streams take care of converting the data into a suitable external format (handling such issues as endianness and Unicode compression transparently ), provided the data is passed to the streams API in a sensible fashion. Clearly, the streams API cannot do much with a pointer ”any objects owned by another must be individually streamed out.

First consider the streams API before considering how to stream a complete object to file.

To use streams functionality, you will need to #include <s32std.h> at the very least. To use file streams (as described next), you will also need s32file.h . You will then need to link against estor.lib by adding it to the LIBRARY line of your mmp file.


RWriteStream

RWriteStream is the base class for externalizing data to a stream. Before it can be used, it is necessary to connect the stream to some form of data sink. This depends upon the type of stream to be used, so RFileWriteStream will be considered for externalizing to a file.

The code snippet below shows how to create an RFileWriteStream . This function assumes that the file KTxtTestFileName does not already exist. If it does (an error value of KErrAlreadyExists is returned by Create() ), use RFileWriteStream::Open() instead.

 RFileWriteStream writeStream; User::LeaveIfError(writeStream.Create(fs, KTxtTestFileName,    EFileWrite)); writeStream.PushL(); 

Note the use of RWriteStream::PushL() to push the stream onto the Cleanup Stack before calling any leaving functions.

The stream can then be used for externalization using either the various write functions (for example, WriteL() , WriteInt32L() , WriteReal64L() and so on), or by using the overloaded << operators defined for explicitly sized simple types. The ExternalizeL() function below illustrates the use of both methods:

 void CChemicalElement::ExternalizeL(RWriteStream& aStream) const    {    aStream << *iName;    aStream << iSymbol;    aStream.WriteUint8L(static_cast<TUint8>(iAtomicNumber));    aStream.WriteUint16L(static_cast<TUint16>(iRelativeAtomicMass));    aStream.WriteUint8L(static_cast<TUint8>(iRadioactive));    aStream.WriteUint8L(static_cast<TUint8>(iType));    } 

Note how it is necessary to explicitly specify the size of the integer being written out, and the use of static_cast to avoid compiler warnings.

Use of operator<< takes care of Unicode compression automatically, and it will also write out the length of the descriptor so that the system knows how much data to read back in at a later point. Any attempt to read a 16-bit descriptor into an 8-bit one will result in a panic. It is also possible to use WriteL() to write a descriptor to a stream, but this introduces complexities for reading that data back (since the size and type are not stored for you) and does not automatically compress Unicode data as operator<< will.

Note that operator<< can also be used for externalizing integers, but it will not work for TInt . You can only use operator<< with explicitly sized integers (or explicitly sized reals).

It is worth stressing at this point that operator<< and operator>> can leave, since they invoke the functions ExternalizeL() and InternalizeL() ! Do not use them in a non-leaving function without using a trap.


Once writing has completed, it is necessary to commit the changes made before the stream can be released. This ensures that any data buffered is written to the stream. Once RWriteStream::Commit() has been called, it is safe to release the stream:

 writeStream.CommitL(); writeStream.Pop(); writeStream.Release(); 

RReadStream

RReadStream is the conceptual opposite of RWriteStream ”it is the base class for internalizing data from a stream. It must be connected to a data source prior to reading. The example below shows how the file stream created earlier might be opened.

 RFileReadStream readStream; User::LeaveIfError(readStream.Open(fs, KTxtTestFileName, EFileRead)); readStream.PushL(); 

Once opened, data can be internalized using either operator>> or the appropriate ReadXxxL() function. Naturally care must be taken to ensure that data members are read in from the stream in precisely the order in which they were written out. In this case, that means the element name followed by its symbol:

 iName = HBufC::NewL(aStream, KMaxTInt); TPtr modifiableSymbol = iSymbol.Des(); aStream >> modifiableSymbol; 

The first line may be a little surprising ”but there is an overload for HBufC::NewL() which takes a stream. This factory function assumes that the size of the descriptor was streamed out first, followed by the data itself. Luckily, this is exactly what operator<< did in the earlier example.

The second argument is the maximum amount of data to be read. Should the length of the data in the stream exceed this value, the NewL() will leave with KErrOverflow . Otherwise a new HBufC is created with exactly the size to contain the data in the stream. This is by far the easiest method of reading in a variable-sized descriptor.

If you choose not to use operator<< to externalize your descriptor, you must externalize the length of the descriptor explicitly. Your internalizer must read this in and use it to construct an HBufC of the necessary size. RReadStream::ReadL() can then be used to fill the descriptor.

Returning to the example code, it is not possible to read data directly into iSymbol , since TBufC has no modifiable API (and so no overloaded operator<< ). Instead a modifiable pointer descriptor is created using TBufC::Des() , and this allows you to access the memory owned by iSymbol . (An alternative approach would be to read data into a temporary TBuf and then assign this to iSymbol .)

Note that any attempt to use operator<< with a data type that is not supported (for example, TBufC ) results in a very obscure error like this:

[View full width]
 
[View full width]
\EPOC32\INCLUDE\s32strm.inl(200) : error C2039: 'InternalizeL' : is not a member of 'TBufC<3>' \EPOC32\INCLUDE\s32strm.inl(243) : see reference to function template instantiation 'void __cdecl DoInternalizeL(class TBufC<3> &,class RReadStream &,class Internalize::Member)' being compiled

It will not even refer to the line of code that caused the error (in this case trying to use operator>> with iSymbol directly). You would be wise to learn to recognize this type of template error and realize that it is likely to imply the use of operator>> with an illegal type.

 iAtomicNumber = aStream.ReadUint8L(); iRelativeAtomicMass = aStream.ReadUint16L(); 

The final two lines of the code read integers in from the stream. Again, the function names denote the explicit integer size. Use of operator>> is possible, but it requires the recipient to be an explicitly sized integer and not a TInt . An error similar to that above would result if operator>> were to be used with a TInt .

ExternalizeL()

Symbian OS convention requires that any object that can be externalized to a stream should provide a publicly accessible ExternalizeL() function, declared like this:

 void ExternalizeL(RWriteStream& aStream) const; 

Earlier you saw an extract from CChemicalElement::ExternalizeL() . Here is the full function:

 void CChemicalElement::ExternalizeL(RWriteStream& aStream) const    {    aStream << *iName;    aStream << iSymbol;    aStream.WriteUint8L(static_cast<TUint8> (iAtomicNumber));    aStream.WriteUint16L(static_cast<TUint16> (iRelativeAtomicMass));    aStream.WriteUint8L(static_cast<TUint8> (iRadioactive));    aStream.WriteUint8L(static_cast<TUint8> (iType));    } 

Such functions are obvious for simple classes like CChemicalElement that do not own other complex classes. For those that do, the solution is to invoke the ExternalizeL() of the owned class from within the owner's ExternalizeL() :

 void CMyComplexClass::ExternalizeL(RWriteStream& aStream) const    {    aStream << iSimpleMember1;    aStream << iSimpleMember2;    iComplexMember1->ExternalizeL(aStream);    iComplexMember2->ExternalizeL(aStream);    // ...    } 

A more complicated example is CElementList::ExternalizeL() :

 void CElementList::ExternalizeL(RWriteStream& aStream) const    {    TInt32 elementCount = NumberOfElements();    aStream << elementCount;    for (TInt i = 0; i < elementCount; i++)       {       At(i).ExternalizeL(aStream);       }    } 

In such a case, it is simply a matter of externalizing the number of items, elementCount , you are about to externalize, and then externalizing the items themselves .

Note that you can also do this using operator<< instead of ExternalizeL() ”any class that has ExternalizeL() defined can use operator<< :

 void CElementList::ExternalizeL(RWriteStream& aStream) const    {    TInt32 elementCount = NumberOfElements();    aStream << elementCount;    for (TInt32 i = 0; i < elementCount; i++)       {       aStream << At(i);       }    } 

It is sometimes a good idea to write out a version number (perhaps just a TInt32 ) to your stream before anything else. This might allow future versions of your application with different stream formats to interpret an older version's streams. At the very least it will allow you to inform the user that the stream belongs to an older version, rather than having the inevitable panic or scrambled data that would result otherwise.


InternalizeL()

If an object can be externalized, you will need to implement an InternalizeL() to do the converse :

 void InternalizeL(RReadStream& aStream); 

Here is CChemicalElement::InternalizeL() :

 void CChemicalElement::InternalizeL(RReadStream& aStream)    {    delete iName;    // Just in case this existed previously    iName = NULL;    iName = HBufC::NewL(aStream, KMaxTInt);    TPtr modifiableSymbol = iSymbol.Des();    aStream >> modifiableSymbol;    iAtomicNumber = aStream.ReadUint8L();    iRelativeAtomicMass = aStream.ReadUint16L();    iRadioactive = static_cast<TBool>(aStream.ReadUint8L());    iType = static_cast<TElementType>(aStream.ReadUint8L());    } 

And CElementList::InternalizeL() :

 void CElementList::InternalizeL(RReadStream& aStream)    {    TInt32 elementCount;    aStream >> elementCount;    for (TInt32 i = 0; i < elementCount; i++)       {       CChemicalElement* newElement = CChemicalElement::NewLC(aStream);       AppendL(newElement);       CleanupStack::Pop(newElement);   // Now owned by array       }    } 

Table 3-7. Store Types

Name

Instantiable

Comments

CStreamStore

No

Abstract base class for all stores, providing functionality for streams to be created and manipulated.

CPersistentStore

No

Abstract base class for persistent stores. It provides the behavior for setting and retrieving the root stream ID.

CFileStore

No

Abstract base class for file-based persistent store. Constructors take either an RFs and filename or an open file.

CDirectFileStore

Yes

Derived from CFileStore . File opened during construction. Enables streams to be created and objects externalized to them. Once committed, streams cannot be replaced , deleted, extended or changed in any way.

CPermanentFileStore

Yes

Derived from CFileStore. File opened during construction. Allows full manipulation of store contents. Generally used by database applications.

CEmbeddedStore

Yes

Derived from CPersistentStore . An embedded store is one that is contained with another stream.

CBufStore

Yes

Derived from CStreamStore . Nonpersistent, in-memory store.

CSecureStore

Yes

Derived from CStreamStore . Streams within the store are all encrypted.


This is a little different. Rather than constructing a new CChemicalElement with dummy values and then internalizing, you can overload the NewLC() factory function to take a stream:

Overloading NewL() and NewLC() to take a Read Stream

Providing overloads such as this gives the simplest possible API for constructing a new object from a stream. It does not get much easier than calling NewL() with a read stream to get a freshly internalized object instantiated on the heap.

The implementation is pretty simple, although it does require the existence of a (presumably private) default constructor:

 CChemicalElement* CChemicalElement::NewLC(RReadStream& aStream)    {    CChemicalElement* self = new (ELeave) CChemicalElement;    CleanupStack::PushL(self);    self->InternalizeL(aStream);    return self;    } CChemicalElement* CChemicalElement::NewL(RReadStream& aStream)    {    CChemicalElement* self = NewLC(aStream);    CleanupStack::Pop(self);    return self;    } 

These are almost identical to the canonical NewL() and NewLC() pair, other than InternalizeL() replacing ConstructL() ”a minimal outlay of effort for a considerable increase in ease of use!

Stores

Stores are basically collections of streams. There are a number of different types of store, each catering for a particular need. For example, sensitive data can be stored using CSecureStore , which encrypts its data streams. Table 3-7 gives a brief description of the available types of store, and Figure 3-10 shows the class hierarchy of stores.

Figure 3-10. Store hierarchy.


To use stores, you need to #include <s32stor.h> .


Stores are used in Symbian OS to save documents associated with applications. This aspect of file functionality is tied up with the Application Framework and is covered in Chapter 4. Many of the basic issues, however, can be discussed here with the focus being on file-based stores.

CFileStore itself is derived from CPersistentStore ”its existence can last beyond that of the application that created it. Contrast this with CBufStore , which cannot be closed without losing all the data it contains.

There are two basic types of CFileStore : CPermanentFileStore and CDirectFileStore . CPermanentFileStore is used by applications that consider the data in the store to be the primary copy of the data. If the application data needs to be modified, it is loaded in from store, amended and then written back. The entire store will never be replaced; individual entries will be inserted, deleted or modified. CPermanentFileStore is typically used by database type applications, particularly those that use the DBMS API. Further details about the use of CPermanentFileStore can be found in the SDK documentation.

In contrast, CDirectFileStore allows no modification of data, once it has been committed. Applications using a direct file store will consider the in-memory version of data to be the primary copy, and modifications will be made to this internal, nonpersistent form of the data. When the data is to be persisted ”for example, when the application exits ”the original file will be completely replaced. Use of CDirectFileStore is explained in detail in the next section.

CDirectFileStore

This is how to create a direct file store, given a connected file server session, a filename and the mode in which to open the store:

 void CElementsEngine::CreateStoreL()    {    CFileStore* store = CDirectFileStore::CreateLC(iFs,       KTxtStoreTestFileName, EFileWrite);    store->SetTypeL(TUidType(store->Layout()));    CleanupStack::Pop();    // Store    iStore = store;    } 

ReplaceLC() can also be used instead of CreateLC() to overwrite an existing file.

When the store is created, its type should be set with a call to SetTypeL() , with the TUidType passed in specifying the UID of the store layout, as returned by the Layout() method of the concrete type (in other words, identifying it as either a direct or permanent file store). The second and third UID components may be used for the store subtype (typically KUidAppDllDoc or KDatabaseUid ) and the application UID, if required by the application.

In the example code shown, only the first UID component (store layout) is used, since .exe s do not have documents associated with them. However, in a UI application, the application UID mapping stored in the file can be used by the system to recognize the application that such a file should be opened with.

Once the type has been set, the store is ready for use.

Closing the store is simply a matter of deleting the store object ”the destructor takes care of closing the actual file and deallocating any resources. Naturally, since this is a persistent store, deleting the store object does not delete the actual file!

To reopen the store at a later stage ”for example, to read it back in ”use OpenL() or OpenLC() :

 CFileStore* store = CDirectFileStore::OpenLC(iFs, KTxtStoreTestFileName, EFileRead); 

If you know the type of store you are dealing with, then you can use the open functions of the required concrete class ”in this case, CDirectFileStore::OpenLC() . If you were to try and open this store using CPermanentFileStore::OpenLC() , then the attempt would leave with KErrNotSupported ( -5 ). Note that if you are unsure of the store type, then the base class ( CFileStore ) implementation can be called to open the store and the type later determined using Layout() .

Once the store has been created or opened for writing, the first task is usually to create a new stream within the store. Remember that a store is a collection of streams.

Here is the code to create a new stream and write to it:

 RStoreWriteStream stream; TStreamId id = stream.CreateLC(*iStore); stream << *list; stream.CommitL(); 

RStoreWriteStream is derived from RWriteStream . This means, of course, that the ExternalizeL() functions developed earlier will take an RStoreWriteStream just as they took an RFileWriteStream . Once you have created the stream, you can write to it in just the same way as before. In this example, operator<< has been used ”which, you will remember, is templated to invoke ExternalizeL() . Once the writing is complete, you can call CommitL() to flush data from the buffer and into the stream.

Notice how the RStoreWriteStream::CreateLC() function returns a TStreamId . The TStreamId object is simply the offset of the stream within the store, but can be considered to be a handle onto the stream, or a "pointer" to it. What do you do with the stream ID? Presumably you need to record it somewhere so that you can refer to it later when you want to read the stream back in ”but where is the best place to put it? The answer is: in the Stream Dictionary .

The Stream Dictionary

The Stream Dictionary is just another stream that stores a mapping between the other stream IDs and their corresponding (stream) UIDs. These 32-bit unique identifiers are not related in any way to the application or store type UIDs. They only have to be unique within the specific file store, and it is up to the developer to specify the UID of each stream. Once specified, the Stream Dictionary can be used to "look up" the location (stream ID) of each stream from its given UID.

This is best considered by way of an example. To write the list of elements into three streams within the store ”one for the metals, one for the semimetals and one for the nonmetals ”requires three UIDs, say:

 const TInt KUidMetalsStreamId = 0x101F613F; const TInt KUidNonmetalsStreamId = 0x101F6140; const TInt KUidSemimetalsStreamId = 0x101F6141; 

The Stream Dictionary will be used to store the stream IDs associated with each UID as shown in Figure 3-11.

Figure 3-11. Using a stream dictionary.


So the Stream Dictionary can be used to look up the stream ID of the given stream. However, since the Stream Dictionary is itself a stream, how do you find the Stream Dictionary's ID? The answer is to store the Stream Dictionary with the root ID of the store, which is always available for any given store.

In summary, then: the store will return its root stream ID, the root stream will lead to the Stream Dictionary ID, and the Stream Dictionary will allow individual stream IDs to be looked up, given their UIDs (which you have set).

Here is the code to instantiate a new Stream Dictionary:

 iRootDictionary = CStreamDictionary::NewL(); 

Each time you create a new stream in the file store, you need to invoke AssignL() on the Stream Dictionary:

 iRootDictionary->AssignL(TUid::Uid(KUidMetalsStreamId),    metalsStreamId); iRootDictionary->AssignL(TUid::Uid(KUidNonmetalsStreamId),    nonmetalsStreamId); iRootDictionary->AssignL(TUid::Uid(KUidSemimetalsStreamId),    semimetalsStreamId); 

passing in the UID for the stream, and the stream ID returned by the function when the stream was created.

Once all the streams have been added, you can then write the Stream Dictionary itself to a stream in the store, and set the store's root ID to point to the Stream Dictionary's stream:

 RStoreWriteStream root; TStreamId id = root.CreateLC(*iStore); root << *iRootDictionary; root.CommitL(); CleanupStack::PopAndDestroy();    // Root iStore->SetRootL(id); iStore->CommitL(); 

Note that with a CDirectFileStore , it is not possible to delete or otherwise change the streams in any other way once they are written. This is not the problem it may appear to be ”the contents of direct file stores are completely replaced each time a change is made. This type of file store is generally used by applications that consider the in-memory copy of their data to be the primary copy. If your application needs to update a store each time data is amended, and the stored data is considered the primary copy ”for example, in a database-style application ”then a Permanent File Store should be used. More information on this topic can be found in the SDK documentation.

Symbian OS defines a dictionary file store class, CDictionaryFileStore . This is not a stream store class; it is traditionally used for handling .ini files, and these are rarely used in Series 60. Do not confuse a direct file store and stream dictionary with a dictionary file store.


Reading from the Store

Here is the complete code to read all of the data back from the three streams plus the Stream Dictionary:

 void CElementsEngine::ReadElementsFromStoreL()    {    // Open the store    CFileStore* store = CDirectFileStore::OpenLC(iFs,       KTxtStoreTestFileName, EFileRead);    // Create and internalize the dictionary from the root stream:    CStreamDictionary* dictionary = CStreamDictionary::NewLC();    RStoreReadStream dictionaryStream;    dictionaryStream.OpenLC(*store, store->Root());    // Stream in the dictionary    dictionaryStream >> *dictionary;    CleanupStack::PopAndDestroy(); // DictionaryStream    // Now use the dictionary to look up the stream ids    TStreamId metalsStreamId = dictionary       ->At(TUid::Uid(KUidMetalsStreamId));    TStreamId nonmetalsStreamId = dictionary       ->At(TUid::Uid(KUidNonmetalsStreamId));    TStreamId semimetalsStreamId = dictionary       ->At(TUid::Uid(KUidSemimetalsStreamId));    CleanupStack::PopAndDestroy(dictionary);    // Read in each stream    RStoreReadStream stream;    stream.OpenLC(*store, metalsStreamId);    stream >> *iElementList;    CleanupStack::PopAndDestroy(); // stream    stream.OpenLC(*store, nonmetalsStreamId);    stream >> *iElementList;    CleanupStack::PopAndDestroy(); // stream    stream.OpenLC(*store, semimetalsStreamId);    stream >> *iElementList;    CleanupStack::PopAndDestroy(); // stream    CleanupStack::PopAndDestroy(store);    } 

Notice how the Root() function on the store is used to obtain the root (Stream Dictionary) stream ID. CStreamDictionary::At() returns the ID of the stream from the given UID.

File Stores Overview

To write multiple streams to a direct file store, use the following steps:

1.
Specify the UIDs for the streams you wish to define (as part of the design of your application) ”these need to be unique within the store.

2.
CreateL() or ReplaceL() the store, specifying a file server session, the filename and EFileWrite .

3.
Set the type of the store using SetType() . Use Layout() to get the store layout UID. (This will be KDirectFileStoreLayout for a direct file store.)

4.
Create a Stream Dictionary object.

5.
Create each of the file store write streams you wish to use, and assign each one to the Stream Dictionary with the appropriate UID.

6.
Externalize the object data to the correct streams.

7.
Commit each store stream once writing is complete.

8.
Once all streams have been written, write the Stream Dictionary to the store.

9.
Commit the Stream Dictionary.

10.
Set the store's root stream to the stream ID of the Stream Dictionary, using SetRootL() .

11.
Commit the store.

12.
Delete the store and Stream Dictionary to free their associated resources.

To read the contents of the store, follow these steps:

1.
OpenL() the file store, specifying a file server session, the filename and EFileRead .

2.
Create a Stream Dictionary object and internalize it from the store. Use Root() on the file store to specify the Stream Dictionary's stream ID.

3.
Retrieve the stream ID of each stream by calling At() on the Stream Dictionary, specifying the UID of the stream.

4.
Internalize each stream from the store.

5.
Delete the store and the Stream Dictionary.

In the example given, there is no clear justification for storing the element list in three separate streams within the file store ”it has been done purely for the sake of example. In a real application, each stream may represent different data objects, such as video and audio streams in a media player, or high scores, player data and map information in a game. Streams allow you to keep this data separate; stores allow you to keep the separate streams in one place.

Further information on the other stores mentioned in this section can be found in the SDK documentation.



Developing Series 60 Applications. A Guide for Symbian OS C++ Developers
Developing Series 60 Applications: A Guide for Symbian OS C++ Developers: A Guide for Symbian OS C++ Developers
ISBN: 0321227220
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net