Thursday, June 12, 2014

Cappuccino Data Serialization

While it is possible to create client-side applications that exist entirely on the client-side and make no communication with a server, this is an unlikely scenario.  At some point, you will want or need to send data back to the server for storage or to initiate data retrieval.

Because Cappuccino is rooted in JavaScript, you can use traditional AJAX methods to make server requests.  You can even step this up a programming notch by loading the jQuery libraries in your index.html file and using jQuery to handle the AJAX, but there must be a Cappuccino way of handling these actions, and there is.

In this article, we will review various data serialization methods; later, we will look at server communication.

Data Serialization

I do not know of any way to communicate objects in their native state.  The alternative is to convert the object into a byte array or string that communicates its structure and attribute values.  This is the nature of Data Serialization.

In Cappuccino, there is one way to serialize data, which is built into the language.  This method creates a 280NPLIST (280 North is the name of the company that first started coding the Cappuccino framework).  This property list format is not interpreted by server-side languages, but it is amazing if you are just passing data for storage and retrieving it later so that all processing is handled by Cappuccino.

If you require the ability to parse and handle data on the server-side in addition to the client-side, then communicating data in a different property list format or JSON notation is more practical.  Cappuccino has methods for interpreting other pList formats as well as XML and JSON notation to make these alternatives more convenient.  Sadly, there is no easy method to generate these formats in the current Framework design.

Working with 280NPLISTs

Cappuccino gives us the CPKeyedArchiver and CPKeyedUnarchiver classes.  Each is derived from the CPCoder class, and each is used in handling 280NPLISTs.  The CPKeyedArchiver creates the format, and the CPKeyedUnarchiver reads the format to produce Cappuccino object results.

The coder is passed to your object for handling, as apposed to handling your object. This gives you the flexibility to control what gets saved and what is not saved.  To enable your classes to interact with the coder, add the following two methods:
  • -(void)encodeWithCoder:(CPCoder)coder
  • -(id)initWithCoder:(CPCoder)coder
In each case, the coder (a.k.a. the data container) is passed to the object.  In encode with coder, the user calls encodeObject:forKey: on the coder to insert data elements at various key locations, just like a CPDictionary.  In initWithCoder:, the method calls decodeObjectForKey: on the coder object in order to retrieve the value that was stored there.  Using these methods, we are able to use the coder like a filing cabinet.  We put all relevant data into the filing cabinet under certain file names, and then later, we attempt to get those items back by recalling the file names that we used previously.

What sets the CPKeyedArchiver apart from a CPDictionary object is this: the archiver is able to output a serialized string in 280NPLIST format to store the data contents in a database or file record. This data is not easily read by human eyes, but it is possible. We will see an example later.

JSON objects

JSON is an easy format to understand, and there are many resources available for those who need to study it.  We will not cover the format here.

The easiest method for converting an object into a JSON string is to call the javascript object method JSON.stringify().  However, this method does not work on all Cappuccino objects; specifically, it has particular difficulty with CPDictionary objects and custom objects. The closer to native javascript that code is, the easier it converts.  CPString, CPInteger, CPArray objects all convert very easily, because these are extensions of the javascript base classes.  CPDictionary however, creates a circular reference which is too much for the JSON object to handle in the stringify() method.

For this reason, it is a good idea to handle JSON creation on a case by case basis. I prefer to create getJSONFriendlyObject methods in my objects or a getJSONString method on the root objects. The getJSONFriendlyObject method can be used for nesting custom objects within other custom objects, and getJSONString is excellent on the base object to get around having to stringify the resulting structure.

Often creating an initWithJSON method is helpful, but for many Cappuccino objects, this method already exists.  The CPDictionary object has an often overlooked method.  Many people notice the instantiation method: +dictionaryWithJSObject:, but there is another one: +dictionaryWithJSObject:recursively:.  This second method allows the CPDictionary to traverse deeper into JSON objects that have multiple levels.  To create the JSON object for this method as well as all the other instantiation methods, simply call the method jsonFromString on the string value that is returned from your stored or communicated JSON text.

Here is an example that I use to extend the CPDictionary class to handle all basic data types.  This method works perfectly well when CPDictionary is holding arrays, strings and numerical values:
@implementation CPDictionary (JSON)
{
}
-(object)getJSONFriendlyObject{
    var keys = [self keyEnumerator],
        object = {},
        string = Nil,
        data = Nil,
        key = Nil;
    while( key = [keys nextObject] )
    {
        string = key;
        data = [self objectForKey:key];
        object[ string ] = data;
    }
    return object;
}
@end
I can now call this method on any CPDictionary (that is being used for simple means) and add the returned result to a basic javascript object.  Then, I can call JSON.stringify( result ) to get the string representation without the complications caused by Cappuccino's sometimes complex structure.

Property List Serialization

CPPropertyListSerialization is a class that creates other types property lists. We can even create a property list in XML format, which is both human readable and easily transferred to other server-side scripting languages.

This Cappuccino object handles CPDictionaries and other Cappuccino objects very well, but it fails when a JavaScript native object is passed.  Another negative aspect of this particular object and encoding method is that it works primarily on CPDictionaries and CPArray objects, so your data must be put into a CPDictionary first, and then passed to the Cappuccino object.

Examples in Code

Our examples need to look at several different scenarios.  We will look at the strengths and weaknesses of each of the methods.

Create a new project.  Remove the contents of the applicationDidFinishLaunching method.  Insert the following code:
    /**********
        test data transfer to and from server
        ********/
    var data = [[StorableData alloc] init],
        jsonObj = {};
    [self fillData:data];
    /*************
         Our data object is set up
         *************/
    var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
        contentView = [theWindow contentView],
        _280NPLIST = [[CPTextField alloc] initWithFrame:CGRectMakeZero()],
        _JSON = [[CPTextField alloc] initWithFrame:CGRectMakeZero()],
        _XML = [[CPTextField alloc] initWithFrame:CGRectMakeZero()],
        _280String = Nil,
        _JSONString = Nil,
        _XMLString = Nil;
    /**************
        Extract the data in three methods.  We use try blocks, for the case that any might fail.
        ***********/
  
    try{
        var coder = [CPKeyedArchiver archivedDataWithRootObject: data];
        _280String = [coder string];
    }catch( e ){
        _280String = "Coder Failed";
    }
  
    try{
        _JSONString = JSON.stringify( data );
        /**** temporarily removed
        var JSONObj = {};
        [data encodeJSON:JSONObj];
        _JSONString = JSON.stringify( JSONObj );
       ****/
    }catch( e ){
        _JSONString = "JSON stringify Failed";
    }
  
    try{
        _XMLString = [CPPropertyListSerialization dataFromPropertyList:data format:CPPropertyListXMLFormat_v1_0 ];
        //_XMLString = [data getXML];
    }catch( e ){
        _XMLString = "PropertyList Failed";
    }
  
    /***********
        Display serialization results
        *******/
      
    [self setStringValue:_280String atOrigin:CGPointMake(10,10) onLabel:_280NPLIST];
    [self setStringValue:_JSONString atOrigin:CGPointMake(410,10) onLabel:_JSON];
    [self setStringValue:_XMLString atOrigin:CGPointMake(810,10) onLabel:_XML];
  
    [contentView addSubview:_280NPLIST];
    [contentView addSubview:_JSON];
    [contentView addSubview:_XML];
    [theWindow orderFront:self];
Then add the following methods to the AppController object:
-(void)setStringValue:(CPString)dataString atOrigin:(CGPoint)frameOrigin onLabel:(CPTextField)label
{
    [label setLineBreakMode:CPLineBreakByWordWrapping];
    [label setFrameSize:CGSizeMake(350,200)];
    [label setStringValue: dataString];
    [label sizeToFit];
    [label setFrameOrigin:frameOrigin];
}
-(void)fillData:(id)data
{
    var array = [@"Array Item 1", 23, { "class":"anObject","data":"someData" }],
        string = @"This is a good string to use.",
        number = 546789.99,
        dict = [[CPDictionary alloc] init];
    //array = [@"Array Item 1", 23];
    [dict setObject:@"Dict Object 1" forKey:@"Object 1"];
    [dict setObject:53 forKey:@"Object 2"];
    [data setAnArray:array];
    [data setAString:string];
    [data setAnInteger:number];
    [data setADictionary:dict];
}
Within the AppController.j file, we will add two object definitions:
@implementation StorableData : CPObject
{
    CPArray anArray @accessors;
    CPString aString @accessors;
    CPInteger anInteger @accessors;
    CPDictionary aDictionary @accessors;
}
-(id)init
{
    if( self = [super init] ){
        anArray = Nil;
        aString = Nil;
        anInteger = Nil;
        aDictionary = Nil;
    }
    return self;
}
-(void)encodeWithCoder:(CPCoder)coder
{
    if( [super respondsToSelector:@selector(encodeWithCoder:)] ){
        //[super encodeWithCoder:coder];
    }
    [coder encodeObject:anArray forKey:@"anArray"];
[coder encodeObject:aString forKey:@"aString"];
[coder encodeObject:anInteger forKey:@"anInteger"];
[coder encodeObject:aDictionary forKey:@"aDictionary"];
}
-(id)initWithCoder:(CPCoder)coder
{
if( self = [super initWithCoder:coder] ){
if( self = [super init] ){
anArray = [coder decodeObjectForKey:@"anArray"];
aString = [coder decodeObjectForKey:@"aString"];
anInteger = [coder decodeObjectForKey:@"anInteger"];
aDictionary = [coder decodeObjectForKey:@"aDictionary"];
}
}
return self;
}
-(void)encodeJSON:(id)object
{
    object.anArray = anArray;
    object.aString = aString;
    object.anInteger = anInteger;
    object.aDictionary = [aDictionary getJSONFriendlyObject];
}
-(object)getXML
{
    console.log( anArray );
    var keys = ["aString","anInteger","aDictionary","anArray"],
        objects = [aString, anInteger,aDictionary, anArray],
        dict = [[CPDictionary alloc] initWithObjects:objects forKeys:keys],
        errorString = Nil;
    console.log( dict );
    var data = [CPPropertyListSerialization dataFromPropertyList:dict format:CPPropertyListXMLFormat_v1_0];
     return [data string];
}
@end
@implementation CPDictionary (JSON)
{
}
-(object)getJSONFriendlyObject{
    var keys = [self keyEnumerator],
        object = {},
        string = Nil,
        data = Nil,
        key = Nil;
    while( key = [keys nextObject] )
    {
        string = key;
        data = [self objectForKey:key];
        object[ string ] = data;
    }
    return object;
}
@end 
Load the application, and you should see the following:
For this first run, we see that the 280NPLIST handles the data perfectly well.  There is no issue with objects of any type.  However, there is difficulty in this.  It is not easily decoded, which means that if we pass the object to a server-side script, all we can really do with it is store it and retrieve it.  Processing this data is not convenient or easy outside having a Cappuccino Keyed Unarchiver decode the text.

The JSON stringify function failed as a result of the CPDictionary object, and the Property List failed to generate, because the data object was passed as a raw, unprocessed element.

Let's adjust this.  Change the try block for JSON to the following:
        //_JSONString = JSON.stringify( data );
        var JSONObj = {};
        [data encodeJSON:JSONObj];
        _JSONString = JSON.stringify( JSONObj );
And change the try block for XML to this:
        //_XMLString = [CPPropertyListSerialization dataFromPropertyList:data format:CPPropertyListXMLFormat_v1_0 ];
        _XMLString = [data getXML];
The JSON object now is what we would expect to see, but the XML property list still fails.

There is more to getting the JSON object to work than altering changing a few lines of commented code. This uncommented code calls a method that is part of the custom data object.  The method takes a simple javascript object and modifies it to model the data we need to store. We model the CPDictionary by extending the class to create a javascript object where each key is a property name and the object associated with the key is the property value.  The returned object is easily worked through stringify(), because it is native javascript.

The XML property list now is using a dictionary as is intended, but it fails anyway. One more alteration, and we will see the cause of the problem.  Uncomment the line of code that reads:

//array = [@"Array Item 1", 23];
When we reload the page, we see the following:
Hooray! We now have a XML property list, but at what cost?

By uncommenting the line of code that we did, we changed the value of the CPArray object and effectively removed just one element of the array.  This one element was causing the failure.  The element was not a Cappuccino object, it was a javascript object defined as follows (in JSON):
var troublesomeObject = {
   "class":"anObject",
   "data":"someData"
}
Naturally, it is up to each of us to know our own data structures, particularly when it comes to storage, so now that we know the limitations of the various serialization methods, we can choose a method that is right for us, or find a way to work around the defects.  None of the issues listed above is insurmountable.

Given the ease of working with JSON, I prefer that as a communication method.  Add a layer of encryption to encode the data in transit, and you have an easily readable, albeit slightly large (as compared to binary) format, that can be made and unmade with little complication but a little extra coding (each JSON object really should be constructed and deconstructed by "hand").

No comments:

Post a Comment