Yesterday, I set up MongoDB and the fireratingdb node.js web server driving it on Windows and started populating the database from a desktop C# .NET add-in via its REST API.
Today, let's complete that, adding the door instance documents to the project data:
Once a project has been added, I need to determine its mongo database id, since the door instances refer to that.
The mongo database id is included in the JSON data returned by the REST API POST request to store the project.
To extract it, I need to parse the JSON data for its _id
entry.
.NET offers several possibilities for serialising and deserialising JSON, e.g. the JavaScriptSerializer
class provided in the System.Web.Script.Serialization namespace that I used to
test serialising the project data.
It is not that hard to implement yourself either, though, so I took a look at an existing
personal JSON parser and
integrated that into my project as JsonParser
.
It parses the JSON string containing a dictionary object into a hash table, from which I can extract the new project database id like this:
string json = GetProjectDataJson( doc ); string jsonResponse = PostJsonData( "projects", json ); object obj = JsonParser.JsonDecode( jsonResponse ); if( null != obj ) { Hashtable d = obj as Hashtable; string project_id = d["_id"] as string; . . .
Just like for the project data, I opted for a hand-written door instance JSON encoder:
/// <summary> /// Retrieve the door instance data to store in /// the external database and return it as a /// dictionary in a JSON formatted string. /// </summary> string GetDoorDataJson( Element door, string project_id, Guid paramGuid ) { Document doc = door.Document; string s = string.Format( "\"_id\": \"{0}\"," + "\"project_id\": \"{1}\"," + "\"level\": \"{2}\"," + "\"tag\": \"{3}\"," + "\"firerating\": {4}", door.UniqueId, project_id, doc.GetElement(door.LevelId).Name, door.get_Parameter( BuiltInParameter.ALL_MODEL_MARK ).AsString(), door.get_Parameter( paramGuid ).AsDouble() ); return "{" + s + "}"; }
It works fine when called for each door like this:
json = GetDoorDataJson( e, project_id, paramGuid );
jsonResponse = PostJsonData( "doors", json );
I am explicitly specifying the mongo database id for the door instances by passing in the existing Revit unique id for it.
This obviously causes an error if the same door is added to the database twice.
In theory, I could – and maybe should – check first whether a given item already exists; if so, find it and PUT, otherwise POST the data.
Reading up on the issue in the Stack Overflow thread on REST POST or PUT best suited for upsert operation, I learn:
According to the HTTP specification, the PUT method requests that the enclosed entity be stored under the supplied request URI. If it refers to an already existing resource, the enclosed entity should be considered as a modified version of the one residing on the origin server. If it does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI.
The defining difference between a PUT and a POST is that PUT is an idempotent operation. If it can safely be executed repeatedly without side effects, than a PUT is a valid restful verb to use.
So, I modified the PostJsonData
that I defined yesterday to
store a document using POST to handle both PUT and POST, and accordingly renamed it to UpsertDocument
:
/// <summary> /// PUT or POST JSON document data to /// the specified mongoDB collection. /// </summary> string UpsertDocument( string collection_name_id, string json, string method ) { string uri = Util.RestApiUri + "/" + collection_name_id; HttpWebRequest request = HttpWebRequest.Create( uri ) as HttpWebRequest; request.ContentType = "application/json; charset=utf-8"; request.Accept = "application/json, text/javascript, */*"; request.Method = method; using( StreamWriter writer = new StreamWriter( request.GetRequestStream() ) ) { writer.Write( json ); } WebResponse response = request.GetResponse(); Stream stream = response.GetResponseStream(); string jsonResponse = string.Empty; using( StreamReader reader = new StreamReader( stream ) ) { while( !reader.EndOfStream ) { jsonResponse += reader.ReadLine(); } } return jsonResponse; }
The first argument specifies either just a mongo collection name, in the case of POST, or includes an appended id, for PUT.
Now I can use it like this to either insert new doors or update existing ones:
json = GetDoorDataJson( e, project_id, paramGuid ); jsonResponse = UpsertDocument( "doors/" + e.UniqueId, json, "PUT" );
The response returned by the PUT operation no longer lists the entire data record, but just says Accepted
instead.
We have one remaining issue to fix.
Currently, our POST operation generates a new project document every time we run it.
We obviously need to check whether a project already exists in the database and update its existing document, if so, before adding a new one.
I already mentioned that it is not completely obvious how to identify a Revit project, which is why we are letting mongo generate a database id for it for us in the first place.
In my simple case, I can use the ProjectInfo singleton's UniqueId.
I'll use that to check whether a project has been added to the database.
If so, I retrieve its database id and use PUT to update its data; otherwise, I create it from scratch using POST.
Therefore, first of all, my node.js database web server needs to implement a new method for looing up a project by ProjectInfo UniqueId.
That is easily achieved by adding this new method to the project controller module:
exports.findByUniqueId = function(req, res){ var uid = req.params.uid; Project.findOne({'projectinfo_uid':uid},function(err, result) { return res.send(result); }); };
I obviously also add the corresponding REST API route is added to routes.js:
module.exports = function(app) { . . . app.get('/api/v1/projects/uid/:uid', projects.findByUniqueId); . . . }
Yet again, I enhance and rename .NET request handler method, now to handle GET as well as PUT and POST, this time renaming it to QueryOrUpsert
:
/// <summary> /// GET, PUT or POST JSON document data from or to /// the specified mongoDB collection. /// </summary> string QueryOrUpsert( string collection_name_id_query, string json, string method ) { string uri = Util.RestApiUri + "/" + collection_name_id_query; HttpWebRequest request = HttpWebRequest.Create( uri ) as HttpWebRequest; request.ContentType = "application/json; charset=utf-8"; request.Accept = "application/json, text/javascript, */*"; request.Timeout = Util.Timeout; request.Method = method; if( 0 < json.Length ) { Debug.Assert( !method.Equals( "GET" ), "content is not allowed with GET" ); using( StreamWriter writer = new StreamWriter( request.GetRequestStream() ) ) { writer.Write( json ); } } WebResponse response = request.GetResponse(); Stream stream = response.GetResponseStream(); string jsonResponse = string.Empty; using( StreamReader reader = new StreamReader( stream ) ) { while( !reader.EndOfStream ) { jsonResponse += reader.ReadLine(); } } return jsonResponse; }
This code uses it to check for the existing project, then either updating it or adding a new one:
object obj; Hashtable d; string project_id; string json = GetProjectDataJson( doc ); string query = "projects/uid/" + doc.ProjectInformation.UniqueId; string jsonResponse = QueryOrUpsert( query, string.Empty, "GET" ); if( 0 < jsonResponse.Length ) { obj = JsonParser.JsonDecode( jsonResponse ); d = obj as Hashtable; project_id = d["_id"] as string; jsonResponse = QueryOrUpsert( "projects/" + project_id, json, "PUT" ); } else { jsonResponse = QueryOrUpsert( "projects", json, "POST" ); }
I incremented the version number of the fireratingdb node.js server to release 0.0.7 in the fireratingdb GitHub repository.
The Revit add-in implementing the C# .NET code discussed above is provided in release 2016.0.0.4 in the FireRatingCloud GitHub repository.
Now, obviously, the next step is to read back data from the database and update the externally modified door instance fire rating values in the Revit building model.