Collaboration Store, Part XXVIII

“The best way out is always through.”
Robert Frost

Now that we have completed the functions that send the application record and version record over to the Host instance, the last thing that we need to do is to send over the Update Set XML file attached to the version record. For sending over an attachment, we will need to use the attachment REST API instead of the standard table REST API. Since the contents of the file that we are sending over is plain text, we can use a little hackery to bypass the need to send over an actual file and just place the text content in the body of the request. But first, as usual, we need to grab the GlideRecord that we want to send before we do anything else.

var gsa = new GlideSysAttachment();
var sysAttGR = new GlideRecord('sys_attachment');
if (sysAttGR.get(answer.attachmentId)) {
	...
} else {
	answer = this.processError(answer, 'Invalid attachment record sys_id: ' + answer.attachmentId);
}

Once we have obtained the record, we will need to grab the usual suspects from the System Properties and then construct the appropriate URL for the request.

var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var url = 'https://';
url += host;
url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
url += answer.hostVerId;
url += '&file_name=';
url += sysAttGR.getDisplayValue('file_name');

Next, we will need to construct and configure our sn_ws.RESTMessageV2 object.

var request  = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader('Content-Type', sysAttGR.getDisplayValue('content_type'));
request.setRequestHeader('Accept', 'application/json');
request.setHttpMethod('post');
request.setEndpoint(url);
request.setRequestBody(gsa.getContent(sysAttGR));

Two things to note on this one: 1) the Content-Type header sets the value of the content_type property of the target instance sys_attachment record, and 2) we use our old friend, the GlideSysAttachment utility to obtain the actual XML of the attachment in lieu of a real file (the file contents are not actually a part of the GlideRecord for the sys_attachment table, hence the need to utilize GlideSysAttachment).

Now all we need to do is to execute the request and check the response.

var response = request.execute();
if (response.haveError()) {
	answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 201) {
	answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
}

Unlike the application and version records, we have no need for any information from the returned JSON string, so there is no need to attempt to parse it and pull out any data. As long as there are no errors, we are good to go.

Putting it all together, the entire function looks like this:

processPhase7: function(answer) {
	var gsa = new GlideSysAttachment();
	var sysAttGR = new GlideRecord('sys_attachment');
	if (sysAttGR.get(answer.attachmentId)) {
		var host = gs.getProperty('x_11556_col_store.host_instance');
		var token = gs.getProperty('x_11556_col_store.active_token');
		var url = 'https://';
		url += host;
		url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
		url += answer.hostVerId;
		url += '&file_name=';
		url += sysAttGR.getDisplayValue('file_name');
		var request  = new sn_ws.RESTMessageV2();
		request.setBasicAuth(this.WORKER_ROOT + host, token);
		request.setRequestHeader('Content-Type', sysAttGR.getDisplayValue('content_type'));
		request.setRequestHeader('Accept', 'application/json');
		request.setHttpMethod('post');
		request.setEndpoint(url);
		request.setRequestBody(gsa.getContent(sysAttGR));
		var response = request.execute();
		if (response.haveError()) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() != 201) {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
		}
	} else {
		answer = this.processError(answer, 'Invalid attachment record sys_id: ' + answer.attachmentId);
	}

	return answer;
},

Unfortunately, when I took it out for a spin, it didn’t work. When I was nosing around to see what the source of the problem was, I figured out that the sys_id value that I was using for the attachment GlideRecord was not a string, but an array of comma-separated sys_id pairs, one for the original attachment and one for the copied attachment. This value came out of the fourth step, where we copied the attachment from the original scoped application record to the version record. Once I realized the actual format of the data returned from the GlideSysAttachment copy function, I did a little rewriting of the processPhase4 function to accommodate the actual structure of the returned data.

processPhase4: function(answer) {
	var gsa = new GlideSysAttachment();
	var values = gsa.copy('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId);
	gsa.deleteAttachment(answer.attachmentId);
	if (values[0]) {
		var ids = values[0].split(',');
		if (ids[1]) {
			answer.attachmentId = ids[1];
		} else {
			answer = this.processError(answer, 'Unrecognizable response from attachment copy: ' + values);
		}
	} else {
		answer = this.processError(answer, 'Unrecognizable response from attachment copy: ' + values);
	}
		
	return answer;
},

While I was at it, I went ahead and did a little work on the processError function to add a few diagnostic breadcrumbs to the system log whenever there is an error. That function now looks like this:

processError: function(answer, message) {
	gs.info('ApplicationPublisher.processError: ' + message);
	gs.info('ApplicationPublisher.processError: ' + JSON.stringify(answer));
	gs.addErrorMessage(message);
	answer.error = message;
	return answer;
},

Once I straightened all of that out, everything finally worked as intended. This is the last step in the process, so this essentially completes the code for publishing an application to the Collaboration Store. At this point, I should probably cut another Update Set so that the folks who would like to participate in the testing can take things out for a little test drive. I still need to address the issues with the set-up process uncovered by the last round of testing, so I think I will take that on next time out and then release a new Update Set for those of you who are willing to put things through their paces and report your results. As always, all feedback is very much appreciated.

Collaboration Store, Part XXVII

“It does not matter how slowly you go as long as you do not stop.”
Confucius

Last time we pushed the application record to the Host instance and now we have to do basically the same thing with the version record. The only difference really, other than the table and fields, is that a version record will always be a new record, so there is no need to determine if the record exists or not on the Host instance. This simplifies the code quite a bit. We still need to fetch the GlideRecord that will be sent over, so as we did with the application record, this will be the first order of business.

var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
if (versionGR.get(answer.versionId)) {
	...
} else {
	answer = this.processError(answer, 'Invalid version record sys_id: ' + answer.versionId);
}

Once we have the record, we can gather up our System Properties and build the payload for the REST API call.

var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var payload = {};
payload.member_application = answer.hostAppId;
payload.version = versionGR.getDisplayValue('version');

With our payload in hand, we can now create and configure our sn_ws.RESTMessageV2 object.

var request  = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");
request.setHttpMethod('post');
request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application_version');
request.setRequestBody(JSON.stringify(payload, null, '\t'));

Now all that is left to do is to execute the request and check the response.

response = request.execute();
if (response.haveError()) {
	answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 201) {
	answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
} else {
	var jsonString = response.getBody();
	var jsonObject = {};
	try {
		jsonObject = JSON.parse(jsonString);
	} catch (e) {
		answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
	}
	if (!answer.error) {
		answer.hostVerId = jsonObject.result.sys_id;
	}
}

Well, that was easy! Here’s the whole thing all put together.

processPhase6: function(answer) {
	var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
	if (versionGR.get(answer.versionId)) {
		var host = gs.getProperty('x_11556_col_store.host_instance');
		var token = gs.getProperty('x_11556_col_store.active_token');
		var payload = {};
		payload.member_application = answer.hostAppId;
		payload.version = versionGR.getDisplayValue('version');
		var request  = new sn_ws.RESTMessageV2();
		request.setBasicAuth(this.WORKER_ROOT + host, token);
		request.setRequestHeader("Accept", "application/json");
		request.setHttpMethod('post');
		request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application_version');
		request.setRequestBody(JSON.stringify(payload, null, '\t'));
		response = request.execute();
		if (response.haveError()) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() != 201) {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
		} else {
			var jsonString = response.getBody();
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch (e) {
				answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
			}
			if (!answer.error) {
				answer.hostVerId = jsonObject.result.sys_id;
			}
		}
	} else {
		answer = this.processError(answer, 'Invalid version record sys_id: ' + answer.versionId);
	}

	return answer;
},

That takes care of 6 of the 7 steps. The last one that we will need to do will be to push the Update Set XML attachment over to the Host. That one may be a little more involved, so we will save that for next time out.

Collaboration Store, Part XXVI

“It’s not foresight or hindsight we need. We need sight, plain and simple. We need to see what is right in front of us.”
Gordon Atkinson

Last time, we completed the final step in the publication process for apps published on a Host instance. For those apps published on a Client instance, however, there is still more work to do. Everything that we created locally on the instance where the app is being published needs to be transferred over to the Host. To do that, we can utilize the built-in REST API.

The first thing that we will want to do is to fetch the GlideRecord for the app to obtain all of the data to send over. As we have done in other steps, we will check to make sure that we have obtained the record and report an error if we did not.

var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
if (mbrAppGR.get(answer.mbrAppId)) {
	...
} else {
	answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.appSysId);
}

The next thing that we will want to do is to check to see if the app exists on the Host. Although we have already determined whether or not this app is new to the local instance earlier in the process, we will still want to double check to make sure that there isn’t already a version sitting on the Host for some unknown reason. If we find it, we will want to update it; otherwise, we will want to add it. To find the app on the remote Host, we will use both the name of the app and the name of the instance as query arguments (other instances may have published an app with the same name, but we would not want to update any of those). That’s a fairly standard REST API HTTP get operation.

var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var thisInstance = gs.getProperty('instance_name');
var request  = new sn_ws.RESTMessageV2();
request.setHttpMethod('get');
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");
request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application?sysparm_fields=sys_id&sysparm_query=provider.instance%3D' + thisInstance + '%5Ename%3D' + encodeURIComponent(mbrAppGR.getDisplayValue('name')));
var response = request.execute();
if (response.haveError()) {
	answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() == '200') {
	...
} else {
	answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
}

The HTTP response code should be 200 whether or not any records were returned. To determine if there is a record present on the Host instance, we need to parse the returned JSON string and check the size of the array of records returned.

var jsonString = response.getBody();
var jsonObject = {};
try {
	jsonObject = JSON.parse(jsonString);
} catch (e) {
	answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
}

If we are updating an existing record, the HTTP Method will be a PUT and the URL will include the sys_id of the record. If we are inserting a new record, then the HTTP Method will be a POST. Other than that, the data that we will be sending to the Host instance will be virtually the same, so we can start to build that up before we make the determination as to which method we will use.

var payload = {};
payload.name = mbrAppGR.getDisplayValue('name');
payload.description = mbrAppGR.getDisplayValue('description');
payload.current_version = mbrAppGR.getDisplayValue('current_version');
payload.active = 'true';
request  = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");

Now we can check the size of the array of records returned and handle the things that will be different depending on whether or not this is a new record on the Host.

if (jsonObject.result && jsonObject.result.length > 0) {
	answer.hostAppId = jsonObject.result[0].sys_id;
	request.setHttpMethod('put');
	request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application/' + answer.hostAppId);
} else {
	request.setHttpMethod('post');
	request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application');
	payload.provider = thisInstance;
}
request.setRequestBody(JSON.stringify(payload, null, '\t'));

Now that we have everything all set up, all this is left to do is to execute the method and check the results.

response = request.execute();
if (response.haveError()) {
	answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 200 && response.getStatusCode() != 201) {
	answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
} else {
	jsonString = response.getBody();
	jsonObject = {};
	try {
		jsonObject = JSON.parse(jsonString);
	} catch (e) {
		answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
	}
	if (!answer.error) {
		answer.hostAppId = jsonObject.result.sys_id;
	}
}

If all goes well, the application record will now be present on the Host instance, which will only leave us with two more things to do, add a new version record to the Host instance and then attach the Update Set XML to the version record. Here is the full function for this step in its entirety.

processPhase5: function(answer) {
	var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
	if (mbrAppGR.get(answer.mbrAppId)) {
		var host = gs.getProperty('x_11556_col_store.host_instance');
		var token = gs.getProperty('x_11556_col_store.active_token');
		var thisInstance = gs.getProperty('instance_name');
		var request  = new sn_ws.RESTMessageV2();
		request.setHttpMethod('get');
		request.setBasicAuth(this.WORKER_ROOT + host, token);
		request.setRequestHeader("Accept", "application/json");
		request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application?sysparm_fields=sys_id&sysparm_query=provider.instance%3D' + thisInstance + '%5Ename%3D' + encodeURIComponent(mbrAppGR.getDisplayValue('name')));
		var response = request.execute();
		if (response.haveError()) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() == '200') {
			var jsonString = response.getBody();
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch (e) {
				answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
			}
			if (!answer.error) {
				var payload = {};
				payload.name = mbrAppGR.getDisplayValue('name');
				payload.description = mbrAppGR.getDisplayValue('description');
				payload.current_version = mbrAppGR.getDisplayValue('current_version');
				payload.active = 'true';
				request  = new sn_ws.RESTMessageV2();
				request.setBasicAuth(this.WORKER_ROOT + host, token);
				request.setRequestHeader("Accept", "application/json");
				if (jsonObject.result && jsonObject.result.length > 0) {
					answer.hostAppId = jsonObject.result[0].sys_id;
					request.setHttpMethod('put');
					request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application/' + answer.hostAppId);
				} else {
					request.setHttpMethod('post');
					request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application');
					payload.provider = thisInstance;
				}
				request.setRequestBody(JSON.stringify(payload, null, '\t'));
				response = request.execute();
				if (response.haveError()) {
					answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
				} else if (response.getStatusCode() != 200 && response.getStatusCode() != 201) {
					answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
				} else {
					jsonString = response.getBody();
					jsonObject = {};
					try {
						jsonObject = JSON.parse(jsonString);
					} catch (e) {
						answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
					}
					if (!answer.error) {
						answer.hostAppId = jsonObject.result.sys_id;
					}
				}
			}
		} else {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
		}
	} else {
		answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.appSysId);
	}

	return answer;
},

Next time, we will code out the insertion of the version record, which should be very similar to this step, although it should be a little simpler since we do not have to check to see if a record exists or not. Version records are always new records for each new version published. That should simplify things quite a bit.

Collaboration Store, Part XXV

“Controlling complexity is the essence of computer programming.”
Brian Kernighan

Last time, we realized that we had a little bit of rework to do, and it turns out that we actually have to do a couple of things: 1) insert the missing step (attaching the XML to the version record), and 2) modify the ending point if the instance is the Host instance (there is no need to send the records to the Host instance if you are the Host instance). The first part is easy enough; just insert one more DIV for our missing step and then renumber all of the ones that follow:

<div class="row" id="phase_4" style="visibility: hidden; display: none;">
	<image id="loading_4" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
	<image id="success_4" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<image id="error_4" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<span style="margin-left: 10px; font-weight:bold;">
		Attaching the Update Set XML to the Version record
	</span>
</div>
<div class="row" id="phase_5" style="visibility: hidden; display: none;">
	<image id="loading_5" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
	<image id="success_5" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<image id="error_5" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<span style="margin-left: 10px; font-weight:bold;">
		Sending the Application record to the Host instance
	</span>
</div>
<div class="row" id="phase_6" style="visibility: hidden; display: none;">
	<image id="loading_6" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
	<image id="success_6" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<image id="error_6" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<span style="margin-left: 10px; font-weight:bold;">
		Sending the Version record to the Host instance
	</span>
</div>
<div class="row" id="phase_7" style="visibility:hidden; display: none;">
	<image id="loading_7" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
	<image id="success_7" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<image id="error_7" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<span style="margin-left: 10px; font-weight:bold;">
		Sending the Update Set XML to the Host instance
	</span>
</div>

And of course, we have to insert the missing step in our Script Include, which at this point is just another empty placeholder like all of the others. We’ll build out the details later as we come to that step.

For controlling the point at which we stop doing stuff, we will need to know if this instance is the Host instance or one of the Client instances. The easiest way to do that is to compare our scoped Host instance property with the stock instance_name property.

var isHost = gs.getProperty('instance_name') == gs.getProperty('x_11556_col_store.host_instance');

Then we just need to modify our original conditional statement that only assumed 6 steps and looked like this:

if (answer.phase < 7) {

… to one that will do all 7 steps for a Client instance and only the first 4 steps for a Host instance.

if (answer.phase < 5 || (answer.phase < 8 && !answer.isHost)) {

With that out of the way, we can return to building out the missing steps in the Script Include, starting with the newly added fourth step, which is to attach the Update Set XML to the version record. As you may recall, we already created an attachment record when we generated the XML, so now all we need to do is to transfer that attachment to our new version record. For that, we can return to our old friend, the GlideSysAttachment. This time, instead of creating the attachment record, we will be copying the attachment from one record to another.

var gsa = new GlideSysAttachment();
var newSysId = gsa.copy('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId);

Once we have copied the attachment from the Scoped Application record to the version record, we will want to delete the attachment record linked to the Scoped Application.

gsa.deleteAttachment(answer.attachmentId);

The last thing that we will need to do will be to update the attachment sys_id in our transfer object so that we will have the ID of the right attachment later on when we go to send it over to the Host.

answer.attachmentId = newSysId;

That makes this a relatively simple step in terms of code. The whole thing looks like this:

processPhase4: function(answer) {
	var gsa = new GlideSysAttachment();
	var newSysId = gsa.copy('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId);
	gsa.deleteAttachment(answer.attachmentId);
	answer.attachmentId = newSysId;

	return answer;
},

If the instance doing the publishing is a Host instance, this would actually be the end of the process, as far as publishing is concerned. We will still have to notify all of the other instances of the new version, but that’s an entirely separate process that we will deal with at some point in the future. But for our new Publish to Collaboration Store action, this is all that needs to be done if you are the Host. For all of the other instances, the application, version, and attachment records will all have to be sent over to the Host as a part of this process. We’ll get started on those steps of the process next time out.

Collaboration Store, Part XXIV

“It is a myth that we can get systems ‘right the first time.’ Instead, we should implement only today’s stories, then refactor and expand the system to implement new stories tomorrow. This is the essence of iterative and incremental agility. Test-driven development, refactoring, and the clean code they produce make this work at the code level.”
Robert Cecil Martin (Uncle Bob)

In our last installment, we restructured the way in which the client-side code interacted with the server-side Script Include and then we went ahead and built out that first step. Now we just need to keep marching down through the list of steps until we have tackled them all. The next step in the list is to either update the existing member application record, or in the case of a first-time publication, create a brand new member application record. To build that record, we will need to grab the data from the original sys_app record, we should grab that guy right off of the bat.

processPhase2: function(answer) {
	var sysAppGR = new GlideRecord('sys_app');
	if (sysAppGR.get(answer.appSysId)) {
		...
	} else {
		answer = this.processError(answer, 'Invalid Application sys_id: ' + answer.appSysId);
	}

	return answer;
},

As we did before, we throw in a simple check, just to make sure that we were actually able to fetch the record. The next thing that we will need to do is to see if this is a new record or not, and if so, create that initial entry.

var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
if (answer.mbrAppId == 'new') {
	mbrAppGR.initialize();
	mbrAppGR.provider.setDisplayValue(gs.getProperty('instance_name'));
	mbrAppGR.setValue('application', answer.appSysId);
	mbrAppGR.insert();
	answer.mbrAppId = mbrAppGR.getUniqueValue();
	mbrAppGR.initialize();
}

With a new record created if needed, the rest of the update logic can work for both new and existing records, since we just converted the new record to an existing record by inserting the bare minimum data to kick things off.

if (mbrAppGR.get(answer.mbrAppId)) {
	mbrAppGR.setValue('name', sysAppGR.getValue('name'));
	mbrAppGR.setValue('description', sysAppGR.getValue('short_description'));
	mbrAppGR.setValue('current_version', sysAppGR.getValue('version'));
	mbrAppGR.setValue('active', true);
	mbrAppGR.update();
} else {
	answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.mbrAppId);
}

That’s all there is to that. Putting it all together, the entire processPhase2 function looks like this:

processPhase2: function(answer) {
	var sysAppGR = new GlideRecord('sys_app');
	if (sysAppGR.get(answer.appSysId)) {
		var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
		if (answer.mbrAppId == 'new') {
			mbrAppGR.initialize();
			mbrAppGR.provider.setDisplayValue(gs.getProperty('instance_name'));
			mbrAppGR.setValue('application', answer.appSysId);
			mbrAppGR.insert();
			answer.mbrAppId = mbrAppGR.getUniqueValue();
			mbrAppGR.initialize();
		}
		if (mbrAppGR.get(answer.mbrAppId)) {
			mbrAppGR.setValue('name', sysAppGR.getValue('name'));
			mbrAppGR.setValue('description', sysAppGR.getValue('short_description'));
			mbrAppGR.setValue('current_version', sysAppGR.getValue('version'));
			mbrAppGR.setValue('active', true);
			mbrAppGR.update();
		} else {
			answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.mbrAppId);
		}
	} else {
		answer = this.processError(answer, 'Invalid Application sys_id: ' + answer.appSysId);
	}

	return answer;
},

That one was pretty easy compared to what we went through last time, so let’s go ahead and do another. The next step, which is to create the application version record, is even easier, as every version record is an insert, so we don’t have to check for new or existing.

processPhase3: function(answer) {
	var sysAppGR = new GlideRecord('sys_app');
	if (sysAppGR.get(answer.appSysId)) {
		var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
		versionGR.setValue('member_application', answer.mbrAppId);
		versionGR.setValue('version', sysAppGR.getValue('version'));
		versionGR.setValue('installed', true);
		versionGR.insert();
		answer.versionId = versionGR.getUniqueValue();
	} else {
		answer = this.processError(answer, 'Invalid Application sys_id: ' + answer.appSysId);
	}

	return answer;
},

The next thing that we need to do is attach the XML to the version record now that it exists. This is actually something that was not on our original list of things to do, so before we jump into that, we are going to need to correct that oversight. That’s a little bit of rework to code that we have already had to rework a few times already, but that’s the way these things go sometimes. I’m not really feeling all that excited about reworking that one more time right at the moment, though, so let’s say we jump right into that next time out.

Collaboration Store, Part XXIII

“Everything you can imagine is real.”
Pablo Picasso

Last time, we completed the work on the UI Page for our modal pop-up and tested it out with a stubbed-out version of our server-side Script Include. Now we need to go back into our Script Include and put in the actual code that will do all of the work of publishing an app to the Host instance. In our stubbed-out version, our processPhase function simply returned the value ‘success’ for every call. We need to add a little structure to that function so that each phase can have an exclusive function of its own to contain all of the logic for that particular phase. We can do that by examining the phase variable, and then we can create separate functions for each phase. At this point, we can even stub those out as we did the original, just to allow us to build and test one function at at time. Here is the modified portion of the script:

processPhase: function(phase, mbrAppId, appSysId, updSetId, origAppId) {
	var answer = '';

	if (phase == 1) {
		answer = this.processPhase1(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 2) {
		answer = this.processPhase2(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 3) {
		answer = this.processPhase3(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 4) {
		answer = this.processPhase4(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 5) {
		answer = this.processPhase5(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 6) {
		answer = this.processPhase6(mbrAppId, appSysId, updSetId, origAppId);
	} else {
		gs.addErrorMessage('Invalid phase error; invalid phase: ' + phase);
		answer = 'Invalid phase error; invalid phase: ' + phase;
	}

	return answer;
},

processPhase1: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase2: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase3: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase4: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase5: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase6: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

At this point, we can even run our test again, just to be sure that we did not break anything, and it still should step through all of the tasks and reveal the Done button at the end. So far, so good. Now we can tackle each step one at a time, and work our way through the various processes until we complete them all. We might as well take them all in order, so let’s start out with the processPhase1 function.

The purpose of this first step will be to turn the Update Set into XML. The first thing that we will need to do is to use the passed Update Set sys_id to get the GlideRecord for the Update Set, which we will need to pass to our global function that does all of the heavy lifting. Once we successfully produce the XML, we will have to do something with it temporarily, since we do not yet have the version record to which it will eventually be attached. The simplest thing to do would be to attach it to some record that we do have, and then transfer the attachment once we create the version record. This should work, but it would be nice to send back the sys_id of the new attachment record, just to make things easier in the future step. If we want to do that, though, we will have to change our strategy for the response from a simple string to a JSON string that can have multiple values. That’s not that much of a change, and it sounds like something that will be useful to have in this process, so let’s just go ahead and do that now.

If we create an object that contains all of the values that we have been passing around, that will actually simplify the function calls, as there will only be the one object to pass as an argument. This will change our onload function to this:

function onLoad() {
	var answer = {};
	answer.phase = 1;
	answer.mbrAppId = gel('mbr_app_id').value;
	answer.appSysId = gel('app_sys_id').value;
	answer.updSetId = gel('upd_set_id').value;
	answer.origAppId = gel('mbr_app_id').value;
	processPhase(answer);
}

It will also change our processPhase function to accept the object as the lone argument, and to both send and receive a stringified version of the object with the GlideAjax call.

function processPhase(answer) {
	var ga = new GlideAjax('ApplicationPublisher');
	ga.addParam('sysparm_name', 'processPhaseClient');
	ga.addParam('sysparm_json', JSON.stringify(answer));
	ga.getXMLAnswer(function (jsonString) {
		hideElement('loading_' + answer.phase);
		answer = JSON.parse(jsonString);
		if (answer.error) {
			showElement('error_' + answer.phase);
			showElement('done_button');
		} else {
			showElement('success_' + answer.phase);
			answer.phase++;
			if (answer.phase < 7) {
				showElement('phase_' + answer.phase);
				processPhase(answer);
			} else {
				showElement('done_button');
			}
		}
	});
}

I actually like this much better, but now we are going to have to modify our Script Include to expect the JSON string coming in and also to send a JSON string back. The modified Script Include now looks like this:

var ApplicationPublisher = Class.create();
ApplicationPublisher.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

	processPhaseClient: function() {
		var jsonString = this.getParameter('sysparm_json');
		var answer = {};
		try {
			answer = JSON.parse(jsonString);
			answer = this.processPhase(answer);
		} catch (e) {
			answer = this.processError({}, 'Unparsable JSON string parameter: ' + jsonString);
		}
		
		return JSON.stringify(answer);
	},

	processPhase: function(answer) {
		if (answer.phase == 1) {
			answer = this.processPhase1(answer);
		} else if (answer.phase == 2) {
			answer = this.processPhase2(answer);
		} else if (answer.phase == 3) {
			answer = this.processPhase3(answer);
		} else if (answer.phase == 4) {
			answer = this.processPhase4(answer);
		} else if (answer.phase == 5) {
			answer = this.processPhase5(answer);
		} else if (answer.phase == 6) {
			answer = this.processPhase6(answer);
		} else {
			answer = this.processError(answer, 'Invalid phase error; invalid phase: ' + phase);
		}

		return answer;
	},

	processPhase1: function(answer) {
		return answer;
	},

	processPhase2: function(answer) {
		return answer;
	},

	processPhase3: function(answer) {
		return answer;
	},

	processPhase4: function(answer) {
		return answer;
	},

	processPhase5: function(answer) {
		return answer;
	},

	processPhase6: function(answer) {
		return answer;
	},

	processError: function(answer, message) {
		gs.addErrorMessage(message);
		answer.error = message;
		return answer;
	},

	type: 'ApplicationPublisher'
});

I also went ahead and added a processError function to script to consolidate all of the code related to reporting an issue with any of the processes. Again, I think this is much better than the original, as it both simplifies the code and opens possibilities that did not exist with the original design. Before we get back to coding out that initial phase, we should run another test, just to make sure things are still working as they should.

Rerunning the concept test after modifying the UI Page and the Script Include

Well, at lease we did not break anything. Now let’s get back to work on that processPhase1 function. First, we need to fetch the GlideRecord for the Update Set.

var updateSetGR = new GlideRecord('sys_update_set');
if (updateSetGR.get(answer.updSetId)) {
	...
} else {
	answer = this.processError(answer, 'Invalid Update Set sys_id: ' + answer.updSetId);
}

There is no reason to expect that we would not retrieve the Update Set GlideRecord at this point, but just in case, we check for that anyway and report an error if something is amiss. With the GlideRecord in hand, we can now call on our global utility to turn the Update Set into XML.

var csgu = new global.CollaborationStoreGlobalUtils();
var xml = csgu.updateSetToXML(updateSetGR);

Our global utility does not report any kind of error, but we should probably examine the XML returned, just to make sure that we have a valid XML file. We should be able to do that by checking the first line for the standard XML header.

if (xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>')) {
	...
} else {
	answer = this.processError(answer, 'Invalid XML file returned from subroutine');
}

Now that we have the XML, we need to stuff it somewhere until we need it in a future step. We should be able to go ahead and create the attachment record at this point, and then we can just move the attachment to its proper place once we reach that point. To do that, we can take advantage of the GlideSysAttachment API. One of the things that we will need in order to do that is a name for our new XML file, which we should be able to generate from some details in the original application record, so we will have to go fetch that guy first, and then build the file name from there.

var sysAppGR = new GlideRecord('sys_app');
if (sysAppGR.get(answer.appSysId)) {
	var fileName = sysAppGR.getDisplayValue('name');
	fileName = fileName.toLowerCase().replace(/ /g, '_');
	fileName += '_v' + sysAppGR.getDisplayValue('version') + '.xml';
	var gsa = new GlideSysAttachment();
	...
} else {
	answer = this.processError(answer, 'Invalid Application sys_id: ' + answer.appSysId);
}

Once again, there is no reason to expect that we would not retrieve the application GlideRecord at this point, but just in case, we check for that as well, and report any errors. Once we have the record, we build the file name from the app name and the app version. Now we have everything that we need to create the attachment.

answer.attachmentId = gsa.write(sysAppGR, fileName, 'application/xml', xml);
if (!answer.attachmentId) {
	answer = this.processError(answer, 'Unable to create XML file attachment');
}

Now we have generated our XML and stuffed it into an attachment record for later use. All together, the entire function now looks like this:

processPhase1: function(answer) {
	var updateSetGR = new GlideRecord('sys_update_set');
	if (updateSetGR.get(answer.updSetId)) {
		var csgu = new global.CollaborationStoreGlobalUtils();
		var xml = csgu.updateSetToXML(updateSetGR);
		if (xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>')) {
			var sysAppGR = new GlideRecord('sys_app');
			if (sysAppGR.get(answer.appSysId)) {
				var fileName = sysAppGR.getDisplayValue('name');
				fileName = fileName.toLowerCase().replace(/ /g, '_');
				fileName += '_v' + sysAppGR.getDisplayValue('version') + '.xml';
				var gsa = new GlideSysAttachment();
				answer.attachmentId = gsa.write(sysAppGR, fileName, 'application/xml', xml);
				if (!answer.attachmentId) {
					answer = this.processError(answer, 'Unable to create XML file attachment');
				}
			} else {
				answer = this.processError(answer, 'Invalid Application sys_id: ' + answer.appSysId);
			}
		} else {
			answer = this.processError(answer, 'Invalid XML file returned from subroutine');
		}
	} else {
		answer = this.processError(answer, 'Invalid Update Set sys_id: ' + answer.updSetId);
	}

	return answer;
},

Well, that was a bit of work, but hopefully the remaining steps will all be a little easier. Next time out, we can start on the second step, which will be to create or update the Collaboration Store application record.

Collaboration Store, Part XXII

“Doubt is an uncomfortable condition, but certainty is a ridiculous one.”
Voltaire

Now that we have the face of our new pop-up window laid out, it’s time to build out the code underneath that will do all of the work. Since this is all server-side activity, we will need some kind of client accessible Script Include that we can call from the client side for each step of the process. Just to do a little testing on the structure and the process, I created a stubbed-out version of a Script Include that I can call from the client-side code to do a little testing before we get too far into the weeds.

var ApplicationPublisher = Class.create();
ApplicationPublisher.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    processPhaseClient: function() {
		var phase = this.getParameter('sysparm_phase');
		var mbrAppId = this.getParameter('sysparm_mbr_app_id');
		var appSysId = this.getParameter('sysparm_app_sys_id');
		var updSetId = this.getParameter('sysparm_upd_set_id');
		var origAppId = this.getParameter('sysparm_orig_app_id');
		return this.processPhase(phase, mbrAppId, appSysId, updSetId, origAppId);
	},

    processPhase: function(phase, mbrAppId, appSysId, updSetId, origAppId) {
		return 'success';
	},

    type: 'ApplicationPublisher'
});

At this point, all it does is return the value ‘success’ each time that it is called. This will be enough to prove out all of the client-side code before we dig into the actual work on the server side. For now, we just want to prove that the design is functional before we invest too much time in the actual tasks ahead of us.

On the client side, we will want to create a generic function that will work essentially the same for every step of the process. To keep track of where we are, I created a variable called phase, which is basically just the number of the current step. I called this function processPhase.

function processPhase(phase, mbrAppId, appSysId, updSetId, origAppId) {
	var ga = new GlideAjax('ApplicationPublisher');
	ga.addParam('sysparm_name', 'processPhaseClient');
	ga.addParam('sysparm_phase', phase);
	ga.addParam('sysparm_mbr_app_id', mbrAppId);
	ga.addParam('sysparm_app_sys_id', appSysId);
	ga.addParam('sysparm_upd_set_id', updSetId);
	ga.addParam('sysparm_orig_app_id', origAppId);
	ga.getXMLAnswer(function (answer) {
		if (answer == 'success') {
			hideElement('loading_' + phase);
			showElement('success_' + phase);
			phase++;
			if (phase < 7) {
				showElement('phase_' + phase);
				processPhase(phase, mbrAppId, appSysId, updSetId, origAppId);
			} else {
				showElement('done_button');
			}
		} else {
			showElement('done_button');
		}
	});
}

This is a recursive function that calls itself for every step until all six steps have been completed or there was some kind of an error. If the response from the Ajax call is ‘success’, then the loading icon for that step is hidden and replaced with the success icon, and then the phase is incremented. If we haven’t reached the end of the steps, then the HTML block for the next step is revealed and the process is repeated. If we reach the end of the steps, or if the Ajax call returns anything other than success, then the process comes to an end and the Done button is revealed. There is no effort on the client side to communicate any error or completion message to the operator. The assumption is that all messaging will be handled on the server side.

For the hideElement and showElement functions, I just stole some old code from some other component where I needed to do the same thing. There’s not much exciting here, but just for the sake of including everything, here it is:

function showElement(elementName) {
	var elem = gel(elementName);
	elem.style.visibility = 'visible';
	elem.style.display = '';
}

function hideElement(elementName) {
	var elem = gel(elementName);
	elem.style.visibility = 'hidden';
	elem.style.display = 'none';
}

Even though the processPhase function calls itself once it gets going, something has to initially get the ball rolling to kicks things off. For that, we can use an onload function to initialize all of the values and make that first call.

function onLoad() {
	var phase = 1;
	var mbrAppId = gel('mbr_app_id').value;
	var appSysId = gel('app_sys_id').value;
	var updSetId = gel('upd_set_id').value;
	var origAppId = gel('mbr_app_id').value;
	processPhase(phase, mbrAppId, appSysId, updSetId, origAppId);
}

Although that seemed like a good plan, when I ran my first test run, nothing happened. After doing a little online research, I discovered that the onLoad function is not a natural thing for a UI Page, and if I wanted something to run on load, I needed to add just a bit more code.

addLoadEvent(function() {  
	onLoad();
});

That was much better! Now after calling the server side six times in succession, the final rendition of the pop-up screen looked like this:

Pop-up structure test results

That proves that the client-side process functions as we intended and the stubbed-out Script Include is getting called and is returning the hard-coded ‘success’ value. This should now complete the work on the UI Page itself, so all that is left is to finish out the server-side Script Include to perform all of the tasks itemized in our list of steps. Next time, we can get started on the fist step, which will involve calling the global function that we created earlier to convert the Update Set to XML.

Collaboration Store, Part XXI

“Slow, steady progress is better than daily excuses.”
Robin Sharma

Now that we have managed to create an Update Set from our UI Action, it is time to figure out how to best accomplish the rest of the tasks needed to complete the publication of our Scoped Application to the Host instance. We will need another pop-up window to control the rest of the activities and to report the status to the operator, as the original pop-up went away when the progress bar appeared, and the progress bar goes away once you click on the Done button (an unnecessary click for our purposes, which I would like to avoid at some point, but for now, we will leave it alone and focus on other, more important activities). The next pop-up window should be the last, as it will be a fully custom component and we should be able to control all of the events from this point forward.

Here is what I am thinking at this point: We can create a simple UI that lists all of the steps necessary to complete our objective, and reveal them one at a time as we progress through the work involved. We can have three different icons ahead of each item on the list, one for in progress, one for success, and another for failure, and then show/hide the icons based on the current status of each step in the process. To make that work, we would have to contact the server side for each step along the way, updating the UI on the client side with the status returned from each server call. Once we successfully complete the final step, we can reveal a Done button to close the pop-up.

To find some appropriate icons, I entered image_picker.do in the left-hand navigator to bring up the list of all of the images currently in the /images/ directory of the platform.

image_picker.do results

I selected the following icons to represent the associated stages for each step:

  • In progress: /images/loading_anim4.gif
  • Complete: /images/check32.gif
  • Error: /images/delete_row.gif

As with many things on the Now Platform, there are many ways in which we can develop a pop-up dialog. I am most comfortable with building Service Portal widgets, and I have used widgets for UI pop-ups before, so that was a tempting way to go. However, the initial pop-up that we customized for our UI Action was a straight UI Page, so I decided that, if I wanted to keep things consistent, I should build this new pop-up the same way. So I cloned our original publish_app_dialog_cs UI Page to create publish_app_dialog_cs_2. Then I went in to our original UI Page and removed this code:

var notification = {"getAttribute": function(name) {return 'true';}};
CustomEvent.fireTop(GlideUI.UI_NOTIFICATION + '.update_set_change', notification);
window.location.href = "sys_update_set.do?sys_id=" + updateSetId;

… which took you to the newly created Update Set when everything was finished, and replaced it with this:

var dialogClass = GlideModal ? GlideModal : GlideDialogWindow;
var dd2 = new dialogClass("x_11556_col_store_publish_app_dialog_cs_2");
dd2.setTitle(new GwtMessage().getMessage('Publish to Collaboration Store'));
dd2.setPreference('sysparm_mbr_app_id', this.mbrAppId);
dd2.setPreference('sysparm_app_sys_id', this.appId);
dd2.setPreference('sysparm_upd_set_id', updateSetId);
dd2.setWidth(500);
dd2.render();

… which now pops up our newly cloned UI Page in a new modal dialog. Now we actually have to build out the page. To begin, as I usually do, I started with the HTML of the presentation layer, which came out like this:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="true" xmlns:j="jelly:core" xmlns:g="glide" xmlns:g2="null">

<g2:evaluate jelly="true">

var existingVersions = '';
var mbrAppId = jelly.sysparm_mbr_app_id;
var appSysId = jelly.sysparm_app_sys_id;
var updSetId = jelly.sysparm_upd_set_id;
var appAction = 'Updating';
if (mbrAppId == 'new') {
	appAction = 'Creating';
}

</g2:evaluate>

<g:ui_form>
	<style type="text/css">
		#publish_to_update_set_dialog_container input {
			width: 100%;
			margin-bottom: 10px;
		}
		#publish_to_update_set_dialog_container label {
			text-align: right;
		}
	</style>
	<div id="publish_to_collaboration_store_dialog_container">
		<div class="modal-body">
			${gs.getMessage('Completing the process of publishing the application to the Collaboration Store')}
			<input id="mbr_app_id" type="hidden" value="$[mbrAppId]"/>
			<input id="app_sys_id" type="hidden" value="$[appSysId]"/>
			<input id="upd_set_id" type="hidden" value="$[updSetId]"/>
		</div>

		<div style="padding-left: 50px;">
			<div class="row" id="phase_1">
				<image id="loading_1" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_1" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_1" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 5px; font-weight:bold;">
					Converting Update Set to XML
				</span>
			</div>
			<div class="row" id="phase_2" style="visibility: hidden; display: none;">
				<image id="loading_2" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_2" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_2" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					$[appAction] the Application record
				</span>
			</div>
			<div class="row" id="phase_3" style="visibility: hidden; display: none;">
				<image id="loading_3" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_3" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_3" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					Creating the Version record
				</span>
			</div>
			<div class="row" id="phase_4" style="visibility: hidden; display: none;">
				<image id="loading_4" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_4" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_4" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					Sending the Application record to the Host instance
				</span>
			</div>
			<div class="row" id="phase_5" style="visibility: hidden; display: none;">
				<image id="loading_5" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_5" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_5" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					Sending the Version record to the Host instance
				</span>
			</div>
			<div class="row" id="phase_6" style="visibility: hidden; display: none;">
				<image id="loading_6" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
				<image id="success_6" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<image id="error_4" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
				<span style="margin-left: 10px; font-weight:bold;">
					Sending the Update Set XML to the Host instance
				</span>
			</div>
		</div>
	</div>
</g:ui_form>
</j:jelly>

Basically, it is the same block of code repeated 6 times for the six remaining tasks needed to complete the process. Each item in the list contains a description of the step along with our three icons, only one of which should appear at any given time. Initially, the in progress icon is visible as soon as the step becomes visible, and my plan is to hide that one and reveal one of the other two, depending on how things came out once the step has been completed.

Here is how it looks when it first appears:

New pop-up dialog box

That takes care of the easy part. Now we have to put all of the code underneath of the UI to actually do all of the things that need to be done. That’s quite a bit of work, so let’s say we will get started on that next time out.

Collaboration Store, Part XX

“It’s always too soon to quit!”
Norman Vincent Peale

Now that we have the needed global Script Include out of the way, we can turn our attention to building the Publish to Collaboration Store UI Action. Before we do that, though, we are going to need a couple more tables, one for the published applications, and another for the application versions. I won’t waste a lot of time here on the process of building a new table; that’s pretty basic Now Platform stuff that you can find just about anywhere. I also did not spend a lot of time creating the tables. At this point, they are very basic and just enough to get things going. I am sure at some point I will be coming back around and adding more valuable fields, but for right now, I just included the bare necessities to create this initial publication process.

Here is the form for the base application table:

Base Member Application table input form

… and here is the form for the application versions:

Member Application Version table input form

There is an obvious one-to-many relationship between the application record and all of the various version records for an application. In practice, all of the versions would be listed under an application as a Related List on the application form. We can show examples of that once we build up some data.

Now that that is done, we can get back to our UI Action. Since our action is going to be very similar to the stock Publish to Update Set… action, the easiest way to create ours is to pull up the stock action and use the context menu to select Insert and Stay to clone the action.

Select Insert and Stay from the context menu to clone the UI Action

Now we just have to rename the action and update the script, which actually turned out to be more modifications that I had anticipated, primarily because our action is not in the global scope. First, we had to rename the function to avoid a conflict with the original action with which we will be sharing the page. Also, the gel function and the window object are not available to scoped actions, and of course, we needed to point to our own UI Page instead of the original and put in our own title.. Here is the modified script:

function publishToCollaborationStore() {
	var sysId = g_form.getUniqueValue();
	var dialogClass = GlideModal ? GlideModal : GlideDialogWindow;
	var dd = new dialogClass("x_11556_col_store_publish_app_dialog_cs");
	dd.setTitle(new GwtMessage().getMessage('Publish to Collaboration Store'));
	dd.setPreference('sysparm_sys_id', sysId);
	dd.setWidth(500);
	dd.render();
}

I also removed all of the conditionals for now, since some of that code was not available to a scoped UI Action, and we will end up having our own conditions, anyway, with which we can deal later on. Right now, I am still just focused on seeing if I can get all of this to work.

The next thing that we need to do is to build our own UI Page, on which we can also get a head start by cloning the original using the same Insert and Stay method. The modifications here are a little more extensive, as we want to do much more than just create an Update Set. One thing that we will want to do is check to make sure that the version number is not one that has already been published. To support that, I added some code at the top to pull all of the current versions so that I can stick them into a hidden field.

var existingVersions = '';
var mbrAppId = 'new';
var appSysId = jelly.sysparm_sys_id;
var appGR = new GlideRecord('sys_app');
appGR.get(appSysId);
var appName = appGR.getDisplayValue();
var appVersion = appGR.version || '';
var appDescription = appGR.short_description;
var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
if (mbrAppGR.get('application', appSysId)) {
	mbrAppId = mbrAppGR.getUniqueValue();
	var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
	versionGR.addQuery();
	versionGR.query('member_application', mbrAppId);
	while (versionGR.next()) {
		existingVersions += '|' + versionGR.getDisplayValue('version');
	}
	existingVersions += '|';
}

To pass this value to the client side for validation, I added a hidden field to the HTML for the page:

<input id="existing_versions" type="hidden" value="$[existingVersions]"/>

To check the validity on the client side, I retained the original version validity check and then added my own check of the existing versions right underneath using the same alert approach to delivering the bad news:

this.existingVersions = gel('existing_versions').value;
...
var vd = g_form.validators.version;
if (!vd)
	console.log("WARNING.  Cannot find validator for version field in parent form.");
else {
	var answer = vd.call(g_form, this.versionField.value);
	if (answer != true) {
		alert(answer);
		return null;
	}
}
if (this.existingVersions.indexOf('|' + versionField.value + '|') != -1) {
	alert('Version ' + versionField.value + ' has already been published to the Collaboration Store');
	return null;
}

The last thing that we have to change is what happens once the Update Set has been created. In the original version that we cloned, once the Update Set has been produced, the operator is then taken to the Update Set form to see the newly created Update Set. We don’t want to do that, as we still have much work to do to, including the following:

  • Update or Create an Application record
  • Create a new Version record linked to the Application record
  • Convert the Update Set to XML
  • Attach the XML to the Version Record
  • Send all of the updated/created records over to the Host instance, including the attached XML

We will need some kind of vehicle in which to perform all of the these tasks and keep the operator updated as to the progress and success or failure of each one of the steps. That sounds like quite a bit of work, so I think that will be a good topic for our next installment.

Collaboration Store, Part XIX

“I like taking things apart and putting them back together. Tinkering. I’d be a professional tinkerer. Tinkerbell. I think that’s what they’re called.”
Chris Carmack

When we took a peek under the hood of the two UI Actions that produced an Update Set from a Scoped Application and turned it into an XML file, it looked like we could leverage most of the code to accomplish our own objective of publishing an app to the Host instance. Unfortunately, after experimenting a bit with various modifications, it appears that some things simply cannot be done within the scope of our Collaboration Store app. Two of the Script Includes involved can only be invoked from another global component (UpdateSetExport and ExportWithRelatedLists), and the delete statement that is executed after the XML file is produced is also something that can only be done in the global scope. I did not really want to create any components outside of our application scope, but in this instance, it doesn’t look like I have any other option.

Given this new tidbit of information, my new goal is to create a single global component and limit the contents of that component to just the things that are absolutely necessary to be in the global scope. I may even try to create this component in the global scope programmatically as part of the set-up process so that a separate global Update Set is not needed, but for now, let’s just see if we can get it to work. I called my new component CollaborationStoreGlobalUtils, and set the Accessible from to All application scopes so that it could be called from any of our scoped components.

var CollaborationStoreGlobalUtils = Class.create();
CollaborationStoreGlobalUtils.prototype = {
    initialize: function() {
    },

	type: 'CollaborationStoreGlobalUtils'
};

All of the things that needed to be in the global scope seemed to be centered around the process to convert an Update Set to an XML file, so I created a function called updateSetToXML to which I passed the GlideRecord for the Update Set. The purpose of the function was to turn the Update Set into XML and return the XML text. Most of the code I lifted straight out of the Publish to XML UI Action and the Processor that it called.

updateSetToXML: function(updateSetGR) {
	var updateSetExport = new UpdateSetExport();
	var rusSysId = updateSetExport.exportUpdateSet(updateSetGR);
	var remoteUpdateGR = new GlideRecord('sys_remote_update_set');
	remoteUpdateGR.get(rusSysId);
	var exporter = new ExportWithRelatedLists('sys_remote_update_set', rusSysId);
	exporter.addRelatedList('sys_update_xml', 'remote_update_set');
	var fakeResponse = this.responseObject();
	exporter.exportRecords(fakeResponse);
	remoteUpdateGR.deleteRecord();
	return fakeResponse.getOutputStreamText();
}

This is an abbreviated version of the code lifted from the stock components, as I removed things like role checking, the validity checking of various GlideRecords, and the value of passed parameters. My thinking is that those things can all be handled in our scoped components before we get to this point, so this function only needs to contain those things that are required to be in the global scope. Since the code that we are borrowing was intended to be used to send the XML file created to the servlet response output stream, I did have to introduce a little bit of creative hackery to replace the expected g_response object parameter in the exportRecords function with an object of my own design.

To construct an object that would be accepted as valid input to the exportRecords function, I dug through the code in that function looking for all of properties and methods that it was expecting to find. Most of those were basic setter functions used to define the HTTP response, and since we weren’t actually going to have an HTTP response, I was able to just include those in the object as empty functions that did nothing and returned nothing.

addHeader: function() {
},
setHeader: function() {
},
setContentType: function() {
},

The one method that did expect a return was the getOutputStream function, and it was expected to return a valid Java output stream object. Back in my Java developer days, whenever I needed to convert an output stream to a string, I always used the java.io.ByteArrayOutputStream class, so I set up a local variable that was an instance of that class and then added a getter to return that variable.

outputStream: new Packages.java.io.ByteArrayOutputStream(),
getOutputStream: function() {
	return this.outputStream;
}

Utilizing the global Packages object is another thing that cannot be done in a Scoped Application, but since we already made the commitment to build this global Script Include, that didn’t turn out to be a problem here. The last thing that I needed to do was to convert the output stream object to a string once the XML was assembled, so I created this additional nonstandard function to obtain the text:

getOutputStreamText: function() {
	var dataAsString = Packages.java.lang.String(this.outputStream);
	dataAsString = String(dataAsString);
	return dataAsString;
}

That’s the entire fake g_response object, and the function that provides the object looks like this:

responseObject: function() {
	return {
		outputStream: new Packages.java.io.ByteArrayOutputStream(),
		addHeader: function() {
		},
		setHeader: function() {
		},
		setContentType: function() {
		},
		getOutputStream: function() {
			return this.outputStream;
		},
		getOutputStreamText: function() {
			var dataAsString = Packages.java.lang.String(this.outputStream);
			dataAsString = String(dataAsString);
			return dataAsString;
		}
	};
}

So far, those are the only two functions that I have had to include in my global component. The entire thing at this stage of the process now looks like this:

var CollaborationStoreGlobalUtils = Class.create();
CollaborationStoreGlobalUtils.prototype = {
    initialize: function() {
    },

	updateSetToXML: function(updateSetGR) {
		var updateSetExport = new UpdateSetExport();
		var rusSysId = updateSetExport.exportUpdateSet(updateSetGR);
		var remoteUpdateGR = new GlideRecord('sys_remote_update_set');
		remoteUpdateGR.get(rusSysId);
		var exporter = new ExportWithRelatedLists('sys_remote_update_set', rusSysId);
		exporter.addRelatedList('sys_update_xml', 'remote_update_set');
		var fakeResponse = this.responseObject();
		exporter.exportRecords(fakeResponse);
		remoteUpdateGR.deleteRecord();
		return fakeResponse.getOutputStreamText();
	},

	responseObject: function() {
		return {
			outputStream: new Packages.java.io.ByteArrayOutputStream(),
			addHeader: function() {
			},
			setHeader: function() {
			},
			setContentType: function() {
			},
			getOutputStream: function() {
				return this.outputStream;
			},
			getOutputStreamText: function() {
				var dataAsString = Packages.java.lang.String(this.outputStream);
				dataAsString = String(dataAsString);
				return dataAsString;
			}
		};
	},

	type: 'CollaborationStoreGlobalUtils'
};

Now that we have that little piece out of the way, we can start building our Publish to Collaboration Store UI Action and the associated UI Page that will be launched in the modal pop-up when that action is selected. Unless we have some additional feedback from the set-up process testing, we’ll jump right into that next time out.