Table of Contents Previous Next
Logo
Developing a File System Server in Objective‑C : 21.2 Implementing a File System Server
Copyright © 2003-2009 ZeroC, Inc.

21.2 Implementing a File System Server

We have now seen enough of the server-side Objective‑C mapping to implement a server for the file system we developed in Chapter 5. (You may find it useful to review the Slice definition for our file system in Section 5.4 before studying the source code.)
Our server is composed of three source files:
• Server.m
This file contains the server main program.
• FileI.m
This file contains the implementation for the File servants.
• DirectoryI.m
This file contains the implementation for the Directory servants.

21.2.1 The Server main Program

Our server main program, in the file Server.m, uses the structure we saw in Section 20.3:
#import <Ice/Ice.h>
#import <FileI.h>
#import <DirectoryI.h>

#import <Foundation/NSAutoreleasePool.h>

int
main(int argc, char* argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    int status = 1;
    id<ICECommunicator> communicator = nil;
    @try {
        communicator =
            [ICEUtil createCommunicator:&argc argv:argv];

        id<ICEObjectAdapter> adapter =
            [communicator createObjectAdapterWithEndpoints:
                                @"SimpleFilesystem"
                                endpoints:@"default p 10000"];

        // Create the root directory (with name "/" and no parent)
        //
        DirectoryI *root =
            [DirectoryI directoryi:@"/" parent:nil];
        [root activate:adapter];

        // Create a file called "README" in the root directory
        //
        FileI *file = [FileI filei:@"README" parent:root];
        NSMutableArray *text = [NSMutableArray arrayWithObject:
            @"This file system contains a collection of poetry."];
        [file write:text current:nil];
        [file activate:adapter];

        // Create a directory called "Coleridge" in the root dir
        //
        DirectoryI *coleridge =
            [DirectoryI directoryi:@"Coleridge" parent:root];
        [coleridge activate:adapter];

        // Create a file called "Kubla_Khan"
        // in the Coleridge directory
        //
        file = [FileI filei:@"Kubla_Khan" parent:coleridge];
        text = [NSMutableArray arrayWithObjects:
                            @"In Xanadu did Kubla Khan",
                            @"A stately pleasuredome decree:",
                            @"Where Alph, the sacred river, ran",
                            @"Through caverns measureless to man",
                            @"Down to a sunless sea.",
                            nil];
        [file write:text current:nil];
        [file activate:adapter];

        // All objects are created, allow client requests now
        //
        [adapter activate];

        // Wait until we are done
        //
        [communicator waitForShutdown];

        status = 0;
    } @catch (NSException* ex) {
        NSLog(@"%@", ex);
    }

    @try {
        [communicator destroy];
    } @catch (NSException* ex) {
        NSLog(@"%@", ex);
    }

    [pool release];
    return status;
}
There is quite a bit of code here, so let us examine each section in detail:
#import <Ice/Ice.h>
#import <FileI.h>
#import <DirectoryI.h>

#import <Foundation/NSAutoreleasePool.h>
The code includes the header Ice/Ice.h, which contains the definitions for the Ice run time, and the files FileI.h and DirectoryI.h, which contain the definitions of our servant implementations. Because we use an autorelease pool, we need to include Foundation/NSAutoreleasePool.h as well.
The next part of the source code is mostly boiler plate that we saw previously: we create an object adapter, and, towards the end, activate the object adapter and call waitForShutdown, which blocks the calling thread until you call shutdown or destroy on the communicator. (Ice does not make any demands on the main thread, so waitForShutdown simply blocks the calling thread; if you want to use the main thread for other purposes, you are free to do so.)
int
main(int argc, char* argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    int status = 1;
    id<ICECommunicator> communicator = nil;
    @try {
        communicator =
            [ICEUtil createCommunicator:&argc argv:argv];

        id<ICEObjectAdapter> adapter =
            [communicator createObjectAdapterWithEndpoints:
                                @"SimpleFilesystem"
                                endpoints:@"default p 10000"];

        // ...

        // All objects are created, allow client requests now
        //
        [adapter activate];

        // Wait until we are done
        //
        [communicator waitForShutdown];

        status = 0;
    } @catch (NSException* ex) {
        NSLog(@"%@", ex);
    }

    @try {
        [communicator destroy];
    } @catch (NSException* ex) {
        NSLog(@"%@", ex);
    }

    [pool release];
    return status;
}
The interesting part of the code follows the adapter creation: here, the server instantiates a few nodes for our file system to create the structure shown in Figure 21.1.
Figure 21.1. A small file system.
As we will see shortly, the servants for our directories and files are of type DirectoryI and FileI, respectively. The constructor for either type of servant accepts two parameters: the name of the directory or file to be created and the servant for the parent directory. (For the root directory, which has no parent, we pass a nil parent.) Thus, the statement
DirectoryI *root = [DirectoryI directoryi:@"/" parent:nil];
creates the root directory, with the name "/" and no parent directory.
Here is the code that establishes the structure in Figure 21.1:
        // Create the root directory (with name "/" and no parent)
        //
        DirectoryI *root =
            [DirectoryI directoryi:@"/" parent:nil];
        [root activate:adapter];

        // Create a file called "README" in the root directory
        //
        FileI *file = [FileI filei:@"README" parent:root];
        NSMutableArray *text = [NSMutableArray arrayWithObject:
            @"This file system contains a collection of poetry."];
        [file write:text current:nil];
        [file activate:adapter];

        // Create a directory called "Coleridge" in the root dir
        //
        DirectoryI *coleridge =
            [DirectoryI directoryi:@"Coleridge" parent:root];
        [coleridge activate:adapter];

        // Create a file called "Kubla_Khan"
        // in the Coleridge directory
        //
        file = [FileI filei:@"Kubla_Khan" parent:coleridge];
        text = [NSMutableArray arrayWithObjects:
                            @"In Xanadu did Kubla Khan",
                            @"A stately pleasuredome decree:",
                            @"Where Alph, the sacred river, ran",
                            @"Through caverns measureless to man",
                            @"Down to a sunless sea.",
                            nil];
        [file write:text current:nil];
        [file activate:adapter];
We first create the root directory and a file README within the root directory. (Note that we pass the servant for the root directory as the parent pointer when we create the new node of type FileI.)
After creating each servant, the code calls activate on the servant. (We will see the definition of this member function shortly.) The activate member function adds the servant to the ASM.
The next step is to fill the file with text:
        // Create the root directory (with name "/" and no parent)
        //
        DirectoryI *root =
            [DirectoryI directoryi:@"/" parent:nil];
        [root activate:adapter];

        // Create a file called "README" in the root directory
        //
        FileI *file = [FileI filei:@"README" parent:root];
        NSMutableArray *text = [NSMutableArray arrayWithObject:
            @"This file system contains a collection of poetry."];
        [file write:text current:nil];
        [file activate:adapter];

        // Create a directory called "Coleridge" in the root dir
        //
        DirectoryI *coleridge =
            [DirectoryI directoryi:@"Coleridge" parent:root];
        [coleridge activate:adapter];

        // Create a file called "Kubla_Khan"
        // in the Coleridge directory
        //
        file = [FileI filei:@"Kubla_Khan" parent:coleridge];
        text = [NSMutableArray arrayWithObjects:
                            @"In Xanadu did Kubla Khan",
                            @"A stately pleasuredome decree:",
                            @"Where Alph, the sacred river, ran",
                            @"Through caverns measureless to man",
                            @"Down to a sunless sea.",
                            nil];
        [file write:text current:nil];
        [file activate:adapter];
Recall from Section 18.8.3 that Slice string sequences map to NSArray or NSMutableArray, depending on the parameter direction. Here, we instantiate that array and add a line of text to it.
Finally, we call the Slice write operation on our FileI servant by simply writing:
        [file write:text current:nil];
This code is interesting: the server code invokes an operation on one of its own servants. Because the call happens via the pointer to the servant (of type FileI) and not via a proxy (of type id<FilePrx>), the Ice run time does not know that this call is even taking place—such a direct call into a servant is not mediated by the Ice run time in any way and is dispatched as an ordinary Objective‑C function call. The operation implementation in the servant expects a current object. In this case, we pass nil (which is fine because the operation implementation does not use it anyway).
In similar fashion, the remainder of the code creates a subdirectory called Coleridge and, within that directory, a file called Kubla_Khan to complete the structure in Figure 21.1.

21.2.2 The Servant Class Definitions

We must provide servants for the concrete interfaces in our Slice specification, that is, we must provide servants for the File and Directory interfaces in the Objective‑C classes FileI and DirectoryI. This means that our servant classes look as follows:
#import <Filesystem.h>

@interface FileI : FSFile <FSFile>
// ...
@end

@interface DirectoryI : FSDirectory <FSDirectory>
// ...
@end
Each servant class derives from its skeleton class and adopts its skeleton protocol.
We now can think about how to implement our servants. One thing that is common to all nodes is that they have a name and a parent directory. As we saw earlier, we pass these details to a convenience constructor, which also takes care of calling autorelease on the new servant.
In addition, we will use UUIDs as the object identities for files and directories. This relieves us of the need to otherwise come up with a unique identity for each servant (such as path names, which would only complicate our implementation). Because the list operation returns proxies to nodes, and because each proxy carries the identity of the servant it denotes, this means that our servants must store their own identity, so we can create proxies to them when clients ask for them.
For File servants, we also need to store the contents of the file, leading to the following definition for the FileI class:
#import <Filesystem.h>

@class DirectoryI;

@interface FileI : FSFile <FSFile>
{
    @private
        NSString *myName;
        DirectoryI *parent;
        ICEIdentity *ident;
        NSArray *lines;
}

@property(nonatomic, retain) NSString *myName;
@property(nonatomic, retain) DirectoryI *parent;
@property(nonatomic, retain) ICEIdentity *ident;
@property(nonatomic, retain) NSArray *lines;

+(id) filei:(NSString *)name parent:(DirectoryI *)parent;
(void) write:(NSMutableArray *)text
              current:(ICECurrent *)current;
(void) activate:(id<ICEObjectAdapter>)a;
@end
The instance variables store the name, parent node, identity, and the contents of the file. The filei convenience constructor instantiates the servant, remembers the name and parent directory, assigns a new identity, and calls autorelease.
Note that the only Slice operation we have defined here is the write method. This is necessary because, as we saw previously, the code in Server.m calls this method to initialize the files it creates.
For directories, the requirements are similar. They also need to store a name, parent directory, and object identity. Directories are also responsible for keeping track of the child nodes. We can store these nodes in an array of proxies. This leads to the following definition:
#import <Filesystem.h>

@interface DirectoryI : FSDirectory <FSDirectory>
{
    @private
        NSString *myName;
        DirectoryI *parent;
        ICEIdentity *ident;
        NSMutableArray *contents;
}
@property(nonatomic, retain) NSString *myName;
@property(nonatomic, retain) DirectoryI *parent;
@property(nonatomic, retain) ICEIdentity *ident;
@property(nonatomic, retain) NSMutableArray *contents;
        
+(id) directoryi:(NSString *)name parent:(DirectoryI *)parent;
(void) addChild:(id<FSNodePrx>)child;
(void) activate:(id<ICEObjectAdapter>)a;
@end
Because the code in Server.m does not call any Slice operations on directory servants, we have not declared any of the corresponding methods. (We will see the purpose of the addChild method shortly.) As for files, the convenience constructor creates the servant, remembers the name and parent, and assigns an object identity, as well as calling autorelease.

21.2.3 The Servant Implementation

Let us now turn to how to implement each of the methods for our servants.

Implementing FileI

The implementation of the name, read, and write operations for files is trivial, returning or updating the corresponding instance variable:
(NSString *) name:(ICECurrent *)current
{
    return myName;
}

(NSArray *) read:(ICECurrent *)current
{
    return lines;
}

(void) write:(NSMutableArray *)text current:(ICECurrent *)current
{
    self.lines = text;
}
Note that this constitutes the complete implementation of the Slice operations for files.
Here is the convenience constructor:
+(id) filei:(NSString *)name parent:(DirectoryI *)parent
{
    FileI *instance = [[[FileI alloc] init] autorelease];
    if(instance == nil)
    {
        return nil;
    }
    instance.myName = name;
    instance.parent = parent;
    instance.ident = [ICEIdentity
        identity:[ICEUtil generateUUID] category:nil];
    return instance;
}
After allocating and autoreleasing the instance, the constructor initializes the instance variables. The only interesting part of this code is how we create the identity for the servant. generateUUID is a class method of the ICEUtil class that returns a UUID. We assign this UUID to the name member of the identity. (The category is unused—see Section 32.5 for more information.)
We saw earlier that the server calls activate after it creates each servant. Here is the implementation of this method:
(void) activate:(id<ICEObjectAdapter>)a
{
    id<FSNodePrx> thisNode =
        [FSNodePrx uncheckedCast:[a add:self identity:ident]];
    [parent addChild:thisNode];
}
This is how our code informs the Ice run time of the existence of a new servant. The call to add on the object adapter adds the servant and object identity to the adapter’s servant map. In other words, this step creates the link between the object identity (which is embedded in proxies), and the actual Objective‑C class instance that provides the behavior for the Slice operations.
add returns a proxy to the servant, of type id<ICEObjectPrx>. Because the contents instance variable of directory servants stores proxies of type id<FSNodePrx> (and addChild expects a proxy of that type), we down-cast the returned proxy to id<FSNodePrx>. In this case, because we know that the servant we just added to the adapter is indeed a servant that implements the operations on the Slice Node interface, we can use an uncheckedCast.
The call to addChild connects the new file to its parent directory.
Finally, we need a dealloc function so we do not leak the memory for the servant’s instance variables:
(void) dealloc
{
    [myName release];
    [parent release];
    [ident release];
    [lines release];
    [super dealloc];
}

Implementing DirectoryI

The implementation of the Slice operations for directories is just as simple as for files:
(NSString *) name:(ICECurrent *)current
{
    return myName;
}

(NSArray *) list:(ICECurrent *)current
{
    return contents;
}
Because the contents instance variable stores the proxies for child nodes of the directory, the list operation simply returns that variable.
The convenience constructor looks much like the one for file servants:
+(id) directoryi:(NSString *)name parent:(DirectoryI *)parent
{
    DirectoryI *instance =
        [[[DirectoryI alloc] init] autorelease];
    if(instance == nil)
    {
        return nil;
    }
    instance.myName = name;
    instance.parent = parent;
    instance.ident = [ICEIdentity
        identity:(parent ? [ICEUtil generateUUID] : @"RootDir")
        category:nil];
    instance.contents = [[NSMutableArray alloc] init];
    return instance;
}
The only noteworthy differences are that, for the root directory (which has no parent), the code uses "RootDir" as the identity. (As we saw on page 569, the client knows that this is the identity of the root directory and uses it to create its proxy.)
The addChild method connects our nodes into a hierarchy by updating the contents instance variable. That way, each directory knows which nodes are contained in it:
(void) addChild:(id<FSNodePrx>)child
{
     [contents addObject:child];
}
Finally, the activate and dealloc methods are very much like the corresponding methods for files:
(void) activate:(id<ICEObjectAdapter>)a
{
    id<FSNodePrx> thisNode
        = [FSNodePrx uncheckedCast:[a add:self identity:ident]];
    [parent addChild:thisNode];
}

(void) dealloc
{
    [myName release];
    [parent release];
    [ident release];
    [contents release];
    [super dealloc];
}
Table of Contents Previous Next
Logo