Desktop SamplebriefcaseWithFile

The Desktop sample demonstrates using the DABriefcase class to save and then load two DADataTables that may have changes that are waiting to be sent to the server. It also demonstrates using briefcase properties to ensure that the data in the briefcase is valid and if not delete the briefcase.

The Briefcase can be helpful for situations where there isn't a persistent connection to the server and the user needs to be able to quit your app or otherwise work in an offline fashion. Another use of a it might be to aid in the application start-up process, as you can load the data from disk without needing to rely on the server being available.

Getting Started

The Desktop sample is typically located in /Developer/RemObjects Software/Samples/Data Abstract/Desktop/Briefcases, though you may have installed the Data Abstract Cocoa SDK and it's samples in another location.

To build it, you will of course need Xcode, and like all the samples provided with Data Abstract for Cocoa you will need to be running the Relativity Server with the DASamples Domain and the Simple Schema available.

Running the Sample

The UI for the sample has three parts. A toolbar with various commands, a table that displays data either retrieved from the server or from a briefcase and finally a "console" like view where log data from the sample is displayed.

The toolbar with 4 buttons; "Reload", "Apply", "Show in Finder" and "Invalidate Briefcase" along with a drop down box which contains a list of available servers discovered by the ZeroConf discovery system. The "Reload" button retrieves the data again from the server and saves it to a briefcase. The "Apply" button sends any changes to the server to be saved and then a fresh briefcase is saved to the filesystem which will removed the highlighting on the rows. The "Show in Finder" button will open a finder window in the folder that contains the briefcase file. Finally the "Invalidate Briefcase" button will set a boolean which will be checked next time the briefcase is saved to disk and display a information dialog.

When the sample is run, it starts a Zero Conf discovery service that looks for instances of Relativity Server running on the local network. When one is discovered and it is the first server discovered, then it is selected and an attempt is made to load the initial data. Note that if no servers are discovered, then the popup on the toolbar will be empty. You can specify a server address yourself by clicking on the popup and then clicking on "Specify custom URL...".

The sample checks to see if there is a Briefcase file available and if there isn't then data is freshly retrieved from the selected server. If on the other hand a Briefcase is available, then the BRIEFCASE_DATA_VERSION_KEY stored in the briefcase as a property is tested to see if the data is valid. If it isn't valid then the briefcase is deleted and fresh data is retrieved from the server. If the property is valid then the data is retrieved from the briefcase along and the date that data was stored in the briefcase printed to the console view. After that a notification is dispatched that the UI needs to be refreshed.

At this point the UI will be visible with a list of products, the product group they belong to and a minimum and maximum amount available. Unlike the figure above, when the sample is first run no rows will have bold text, which means that there are no changes compared to the server version of the data. (See below)

If you double click on a row, an edit field will appear in the "Min Amount" column of that row allowing you to change the value. Here attempting to edit, or actually editing the value will mark the DADataTableRow as changed and any changed rows will have their text changed to bold.

If you quit the app at this point, those changes will be saved to the briefcase along with the information that the data is changed compared to the server, so that when you next run the app you will see those very same rows marked as changed in the table. The highlighting will only be removed by either reloading the data from the server or by applying the changes to the server.

If you press the "Show in Finder" button you will see a briefcase package (Products.briefcase) that was written to disk when quitting the app, or after the data is retrieved from the server. You can right-click on this package and choose Show package contents, to see the files inside. There will be three files, two with the extension .daBriefcase which are the tables themselves, and a plist file which has the data stored in any briefcase properties.

Examining the Code

The code for creating the connection to the server, retrieving and modifying the data are covered by other samples. In this section we will be largely focusing on the code needed to create the briefcase, store the tables & properties in it and load those tables & properties from disk.

App Structure

The sample is built around four classes; AppDelegate, DataAccess, ServiceAccess and RegisterServiceWindowController.

The DataAccess class handles everything related to interacting with the Data Abstract SDK; including retrieving data from an instance of Relativity Server, applying changes to the server and working with the briefcase. The code you are interested in is located in this class and will be discussed below.

The ServiceAccess class handles the discovery of any instances of Relativity Server that are available on the local network using ROZeroConf which is a feature available with the Remoting SDK that Data Abstract is built upon. ServiceAccess sets up a ROZeroConfBrowser Class object which searches for any servers that broadcast a value matching the value defined for RELATIVITY_SERVICE_NAME. When a service is found, or indeed disappears, the server list is updated and the popup with the list of available servers is also updated. It also handles the registration of custom server addresses.

The RegisterServiceWindowController class is a subclass of NSWindowController which handles the UI aspects of a user manually adding a server url. There is nothing of interest in this class.

Lastly the AppDelegate class handles the primary setup of the application, registers that it will listen for notifications broadcast by the ServiceAccess and DataAccess classes, and acts as a delegate to the RegisterServiceWindowController, DataAccess and NSTableView classes.

Creating the path to store the Briefcase

NSString *appSupportPath = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject];
 NSString *appHomePath = [appSupportPath stringByAppendingPathComponent:APPLICATION_SUPPORT_FOLDER];
 [[NSFileManager defaultManager] createDirectoryAtPath:appHomePath withIntermediateDirectories:YES attributes:nil error:nil];
 [self.delegate logMessage:[NSString stringWithFormat:@"Home folder is %@", appHomePath]];

 // set up briefcase
 self.briefcaseFileName = [appHomePath stringByAppendingPathComponent:BRIEFCASE_FILENAME];
 [self.delegate logMessage:[NSString stringWithFormat:@"Briefcase file name is %@", self.briefcaseFileName]];

The init method of DataAccess setups up the ServiceAccess class and then registers itself as a delegate of the class. After which it creates an NSString named briefcaseFileName which is the full path that will be used to load and save the briefcase.

In the first two lines, an NSString, appHomeFolder, is created that points to the location that the briefcase will be stored in. That string is then passed as a location to the createDirectoryAtPath:withIntermediateDirectories:attributes:error: method of the NSFileManager class to create the location if it doesn't already exist. Finally a new string, briefcaseFileName, is created which is the full path to the briefcase we will be using. You can find BRIEFCASE_NAME defined at the top of DataAccess.m, along with some other defines used later.

Store the tables in a new briefcase

- (void)saveBriefcase {
    DABriefcase * briefcase = [DABriefcase briefcaseWithFolder:self.briefcaseFileName];
    if (self.invalidateBriefcase) {
        NSString * ver = briefcase.properties[BRIEFCASE_DATA_VERSION_KEY];
        if ([@"INVALID" isEqualToString:ver]) {
            [self.delegate logMessage:@"Briefcase already marked as INVALID."];
        } else {
            briefcase.properties[BRIEFCASE_DATA_VERSION_KEY] = @"INVALID";
            [briefcase writeBriefcase];
            [self.delegate logMessage:@"Briefcase has been marked as INVALID."];
        }
    } else {
        [self putTablesToBriefcase:briefcase];
        [briefcase writeBriefcase];
        [self.delegate logMessage:@"Briefcase has been updated."];
    }
}

- (void)putTablesToBriefcase:(DABriefcase *)briefcase {
    [briefcase addTable:self.productGroupsTable];
    [briefcase addTable:self.productsTable];

    briefcase.properties[BRIEFCASE_DATA_VERSION_KEY] = BRIEFCASE_DATA_VERSION;

    NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateStyle:NSDateFormatterLongStyle];
    [dateFormatter setTimeStyle:NSDateFormatterLongStyle];
    briefcase.properties[BRIEFCASE_UPDATE_DATE_KEY] = [dateFormatter stringFromDate:[NSDate date]];
}

In this sample saving the briefcase to disk is handled over two methods. saveBriefcase handles the creation of a DABriefcase and writing it to disk, while the putTablesToBriefcase adds the DADataTable classes we are interested in into the briefcase, along with recording some information as briefcase properties.

As noted in the Briefcase Overview, briefcases can be stored in one of two formats; either as a single .daBriefcase file which contains all of the tables and properties that you set or as a package (folder) where each table is stored in its own .daBriefcase file and the properties are stored in a .plist file. This sample uses the folder format, but if you wanted to use the other type of briefcase you could use the briefcaseWithFile: method instead.

The briefcaseWithFolder: method of DABriefcase will attempt to load into memory the briefcase folder if it exists, otherwise it will create it in memory until such time as you write it to disk.

The value of self.invalidateBriefcase is tested and if true a property (BRIEFCASE_DATA_VERSION_KEY) is set on the briefcase as INVALID and immediately written to disk.

To write the briefcase to the filesystem we need to use the writeBriefcase: method of DABriefcase. When using a folder based briefcase you could save each table as needed by using the writeTable: and writeTables: methods of DAFolderBriefcase instead.

As you can see in the putTablesToBriefcase method, we add two tables self.productGroupsTable and self.productsTable to the briefcase using the addTable: method. Alternatively addTables:, which takes any array of tables, could be used. If a table with the same name is already there, it will be replaced with this new table.

As the briefcase folder might be around from running the sample previously we remove any properties that are currently set with [briefcase.properties removeAllObjects];. Properties are stored in a dictionary, so we can set and retrieve a property by passing its name to the properties object. Here we set two properties, the first is a version number which could be used to prevent loading a briefcase from an old version of the application, and the second is the data at which the briefcase contents were updated.

Loading the briefcase from disk and retrieving the tables


- (void)initialLoading {
    [self.delegate logMessage:@"Initial loading..."];
    [self.delegate logMessage:@"Looking for the local briefcase first..."];
    [self.delegate logMessage:[NSString stringWithFormat:@"Briefcase: %@", self.briefcaseFileName]];
    BOOL briefcaseExists = [[NSFileManager defaultManager] fileExistsAtPath:self.briefcaseFileName];
    if (!briefcaseExists) {
        [self.delegate logMessage:@"No briefcase found. Downloading fresh data..."];
        [self loadFromServer];
    } else {
        [self.delegate logMessage:@"Briefcase has been found..."];
        DABriefcase * briefcase = [DABriefcase briefcaseWithFolder:self.briefcaseFileName];
        [self.delegate logMessage:@"Validating briefcase..."];
        NSString * v = briefcase.properties[BRIEFCASE_DATA_VERSION_KEY];
        if ([BRIEFCASE_DATA_VERSION compare:v] != NSOrderedSame) {

            [self.delegate logMessage:[NSString stringWithFormat:@"Briefcase looks outdated! Application requires version %@, while briefcase has version %@", BRIEFCASE_DATA_VERSION, v]];
            [self.delegate logMessage:@"Clearing briefcase and downloading fresh data..."];
            [briefcase delete];
            [self loadFromServer];
        } else {
            [self.delegate logMessage:[NSString stringWithFormat:@"Briefcase is valid. Version: %@; Last update: %@", v, briefcase.properties[BRIEFCASE_UPDATE_DATE_KEY]]];
            [self.delegate logMessage:@"Loading data from the briefcase..."];
            [self getTablesFromBriefcase:briefcase];
        }
    }
    [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_REFRESH_UI object:self];

    [self.delegate logMessage:@"data is ready."];
}

- (void)getTablesFromBriefcase:(DABriefcase *)briefcase {
    self.productGroupsTable = [briefcase tableNamed:@"ProductGroups"];
    self.productsTable = [briefcase tableNamed:@"Products"];
    [self setupData];
}

These two methods handle loading the briefcase from disk and retrieving the tables from the briefcase based on the BRIEFCASE_DATA_VERSION property stored inside the briefcase.

The initialLoading method tests to see if the briefcase file exists, if not a fresh set of data is retrieved from the server. Otherwise the briefcase is loaded with the briefcaseWithFolder: method and the BRIEFCASE_DATA_VERSION_KEY property retrieved by passing the name of the required property to briefcase.properties. If the property is set to INVALID then the briefcase is deleted from disk using the delete method, otherwise the tables are retrieved with a call to getTablesFromBriefcase:.

The getTablesFromBriefcase: method retrieves each DADataTable from the briefcase using the tableNamed: method.