Persevere, ExtJS 3.0, and you

Recently, I spent some time playing with Persevere, best summed up by its page:

The Persevere Server is an object storage engine and application server (running on Java/Rhino) that provides persistent data storage of dynamic JSON data in an interactive server side JavaScript environment…

Since it speaks JSON HTTP/REST, it’s an excellent candidate for integration with ExtJS. (It happens to ship with Dojo support out of the box, if that’s your fancy.) A few changes to Ext.data.JsonWriter, Ext.data.JsonReader, and Ext.data.HttpProxy are necessary for ExtJS to handle what Persevere sends and expects to receive.

Persevere does not expect the traditional root property wrapper that many frameworks, such as Rails, expect. All relevant properties are sent in a single object literal instead. To cope with this, HttpProxy needs a single line change:

Ext.override(Ext.data.HttpProxy, {
    doRequest : function(action, rs, params, reader, cb, scope, arg) {
        var  o = {
            method: (this.api[action]) ? this.api[action]['method'] : undefined,
            request: {
                callback : cb,
                scope : scope,
                arg : arg
            },
            reader: reader,
            callback : this.createCallback(action, rs),
            scope: this
        };
 
        // Always do this.
        o.jsonData = params;
 
        // Set the connection url.  If this.conn.url is not null here,
        // the user may have overridden the url during a beforeaction event-handler.
        // this.conn.url is nullified after each request.
        if (this.conn.url === null) {
            this.conn.url = this.buildUrl(action, rs);
        }
        else if (this.restful === true && rs instanceof Ext.data.Record && !rs.phantom) {
            this.conn.url += '/' + rs.id;
        }
        if(this.useAjax){
 
            Ext.applyIf(o, this.conn);
 
            // If a currently running request is found for this action, abort it.
            if (this.activeRequest[action]) {
                // Disabled aborting activeRequest while implementing REST.  activeRequest[action] will have to become an array
                //Ext.Ajax.abort(this.activeRequest[action]);
            }
            this.activeRequest[action] = Ext.Ajax.request(o);
        }else{
            this.conn.request(o);
        }
        // request is sent, nullify the connection url in preparation for the next request
        this.conn.url = null;
    }
}

Next, a few changes to how replies are interpreted. Persevere uses the HTTP status code to signal success or failure, so it’s necessary to fake it and move the data payload into the root property that ExtJS expects to see. The response from a DELETE must be handled specially as Persevere sends an empty HTTP body, causing Ext.decode to explode. I removed a variety of exceptions that were thrown for an empty or missing message body.

Ext.override(Ext.data.JsonReader, {
	readResponse : function(action, response) {
		var o = {};
 
		if(response.status >= 200 && response.status <=300) {
			o[this.meta.successProperty] = true;
		}
		else if(response.status == 403) {
			o[this.meta.successProperty] = false;
		}
 
		// Avoid No Content
		if(response.status !== 204) {
			o[this.meta.root] = (response.responseText !== undefined) ? Ext.decode(response.responseText) : response;
			if(!o[this.meta.root]) {
				throw new Ext.data.JsonReader.Error('response');
			}
		}
 
		// These cause trouble.
		// Persevere uses HTTP status codes for success and uses no root.
		/*
		if (Ext.isEmpty(o[this.meta.successProperty])) {
			throw new Ext.data.JsonReader.Error('successProperty-response', this.meta.successProperty);
		}
		// make sure extraction functions are defined.
		// Nonsense for 204, don't do it
		this.ef = this.buildExtractors();
		// Need to return additional status code
		return o;
	}
});

Finally, data needs to go out over the wire in a format Persevere understands for both PUT and POST requests. The changes to Ext.data.JsonWriter ought to handle multiple item PUTs and POSTs which are both supported by Persevere, but I haven’t had a chance to test. The response that comes back will likely require Ext.data.JsonReader to be modified to understand an array response of object literals.

Ext.override(Ext.data.JsonWriter, {
 
	render:function(action, rs, params, data) {
		var f = rs.fields, fi = f.items, fl = f.length, field, val;
		for(var j = 0; j < fl; j++){
			field = fi[j];
			if(field.type == 'int') {
				val = parseInt(rs.data[field.name]);
				data[field.name] = isNaN(val) ? null : val;
			}
		}
		Ext.apply(params, data);
	},
 
	update:function(rs) {
 
		var params = {};
 
		if (Ext.isArray(rs)) {
			var data = [];
			Ext.each(rs, function(val){
				data.push(this.updateRecord(val));
			}, this);
			Ext.apply(params, data);
		}
		else if (rs instanceof Ext.data.Record) {
			Ext.apply(params, this.updateRecord(rs));
		}
		return params;
	},
 
	create:function(rs) {
 
		var params = {};
 
		if (Ext.isArray(rs)) {
			var data = [];
			Ext.each(rs, function(val){
				data.push(this.updateRecord(val));
			}, this);
			Ext.apply(params, data);
		}
		else if (rs instanceof Ext.data.Record) {
			Ext.apply(params, this.updateRecord(rs));
		}
		return params;
	},
 
	toHash:function(rec) {
		var map = rec.fields.map,
			data = {},
			raw = (this.writeAllFields === false && rec.phantom === false) ? rec.getChanges() : rec.data,
			m;
		Ext.iterate(raw, function(prop, value){
			if((m = map[prop])){
				data[m.mapping ? m.mapping : m.name] = value;
			}
		});
		// Blows up sending phantom to persevere
		if(rec.phantom === false) {
			data[this.meta.idProperty] = rec.id;
		}
		return data;
	}
});

The changes to the render method are necessary to ensure integer values are delivered to Persevere as such, instead of as strings. Additionally, a POST with an id included, such as ExtJS phantom id placeholder, will be treated as a PUT. If the id does not exist, Persevere will respond with a 404 Not Found.

In order to actually speak to Persevere, a few additional options are necessary when building an Ext.data.Store. It’s important to notice the crucial Accepts header. The second portion of it specifies the property name that a collection of object literals live in the case of a GET request to the collection URL.

this.path = '/Sometable';
this.store = {
	autoDestroy:true,
	autoSave:false,
	restful:true,
	reader:new Ext.data.JsonReader(
		{
			totalProperty:'totalCount',
			idProperty:'id',
			root:'items',
		},
		Ext.data.Record.create([
			{name:'size', type:'int'},
			{name:'type'},
			{name:'speed', type:'int'}
		])),
	proxy:new Ext.data.HttpProxy({
		url:this.path,
		method:'GET',
		headers:{'Accept':'application/json; collection=items'},
		// bulk PUT && POST are allowed by persevere
		// the update path needs a trailing / for that or
		// the ids come back as id:object/id instead of id
		api:{
			update:this.path,
			read:this.path+'/',
			create:this.path+'/',
			destroy:this.path
		}
	}),
	writer:new Ext.data.JsonWriter({encode:false, writeAllFields:true})
};

The presence or absence of a trailing backslash is quite important. Without it, a GET to the collection URL will return object literals with ids you aren’t expecting:

js>serialize(load('/Item')); 
[
{"id":"Item/3",
        "sn":"abc123aazzzaa",
        "pn":"aa"
},
...]

Notice without the trailing backslash, the id for all items includes the class name, or table name in Persevere speak. You don’t want that, as subsequent PUT or POST requests will append that id to the URL, resulting in something bogus like /Item/Item/3, leaving you quite confused as to why.

While Pesevere supports more interesting acess schemes via the JSON HTTP REST API, such as updating individual properties and complex querying and sorting with JSONQuery, that’s for another time.

A useful method for testing the consequences of using various URLs is with the curl program. It’s available on virtually every platform or can be obtained.

$ curl -H 'content-type: application/json' \
  -H 'accept: application/json; collection=items'
  http://localhost:8080/Item/3

{"id":"3",
"sn":"abc123aazzzaa",
"pn":"aa"
}

6 Comments

  1. Hugues Dubois
    Posted 7/20/2009 at 8:40 am | Permalink

    Hi this is a good post.

    But I have a problem with http get variable “_dc”. So i put “disableCaching” on config of HttpProxy object.

    It is the good way?

  2. Posted 7/20/2009 at 9:15 am | Permalink

    Oh, yes. I had forgotten. You do need disableCaching for it to work. I had to add it manually inside a load callback directly to the proxy.

  3. Hugues Dubois
    Posted 7/20/2009 at 10:27 am | Permalink

    Ok thanks :)

  4. Posted 8/15/2009 at 7:13 am | Permalink

    Most of your overrides will be unnecessary when 3.0.1 is released soon.

    - Have a look at Ext.data.Api#restify
    - Have a close look at latest DataReader, DataWriter, JsonReader, JsonWriter. Lots of code has been removed.

  5. Posted 1/15/2010 at 4:22 am | Permalink

    Hi there,

    I just wanted to say “thank you” for your excellent web site.

    Until now it helped us twice out of real problems in a customer project where we currently evaluate ExtJS with Jersey.

    Thanx again!

  6. Posted 1/19/2010 at 3:33 am | Permalink

    good your post..
    very nice for me..

    but I do not understand..??

Post a Comment

You must be logged in to post a comment.