Collaboration Store, Part LIV

“Real happiness lies in the completion of work using your own brains and skills.”
Soichiro Honda

Last time, we finally got to the point where we were able to actually Commit the Update Set, installing the requested version of the application. That was a major milestone, but we are not done just yet. We still have to update the version and application records with the fact that this version has now been installed. That’s a server-side operation, so we will need to add yet one more client callable function to our installation utilities Script Include. As we did before, we will use the attachment ID to locate the version record and the associated application record and then update them both. We will also need to hunt down the installed application record so that we can link it to the application record in the Collaboration Store database. We can also use data extracted from the version and application records to build a final status message informing the operator that the installation process is now complete.

We’ll call our new function recordInstallation, and start out by creating a response object, defaulting the success property to false, and then grabbing the attachment ID parameter that was passed in the Ajax call.

recordInstallation: function() {
	var answer = {success: false};
	var sysId = this.getParameter('attachment_id');
	if (sysId) {
		...
	} else {
		answer.error = 'Missing required parameter: attachment_id';
	}
	return JSON.stringify(answer);
}

Assuming that we have a sys_id, we will use it to get the attachment record, and then use the data in the attachment record to fetch the version and application records.

var sysAttGR = new GlideRecord('sys_attachment');
if (sysAttGR.get(sysId)) {
	var versionGR = new GlideRecord(sysAttGR.getDisplayValue('table_name'));
	if (versionGR.get(sysAttGR.getDisplayValue('table_sys_id'))) {
		answer.versionId = versionGR.getUniqueValue();
		var applicationGR = versionGR.member_application.getRefRecord();
		...
	} else {
		answer.error = 'Version record not found for sys_id ' + sysAttGR.getDisplayValue('table_sys_id');
	}
} else {
	answer.error = 'Attachment record not found for sys_id ' + sysId;
}

Once we have the version record and the application record in hand, we will need to hunt down the newly installed system application record.

var sysAppGR = new GlideRecord('sys_app');
sysAppGR.addQuery('scope', applicationGR.getValue('scope'));
sysAppGR.query();
if (sysAppGR.next()) {
	...
} else {
	answer.error = 'System application record not found for scope ' + applicationGR.getValue('scope');
}

Once we have the system application record, then we know that the application has been installed, and we can format the final status message.

answer.statusMessage = 'Version <strong>' + versionGR.getDisplayValue('version') + '</strong> of application <strong>' + applicationGR.getDisplayValue('name') + '</strong> installed.';

Now we need to link the installed system application record to our application record.

applicationGR.application = sysAppGR.getUniqueValue();
applicationGR.update();

We also need to mark the version as being installed.

versionGR.installed = true;
versionGR.update();

One other thing that we will need to do is to go through any other version records associated with this application and make sure that none of those are any longer marked as installed.

versionGR.initialize();
versionGR.addQuery('member_application', applicationGR.getUniqueValue());
versionGR.addQuery('sys_id', '!=', answer.versionId);
versionGR.query();
while (versionGR.next()) {
	versionGR.installed = false;
	versionGR.update();
}

That completes the updates to the Collaboration Store records, so the only thing left to do at this point is to override our initial response object success value with true.

answer.success = true;

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

recordInstallation: function() {
	var answer = {success: false};
	var sysId = this.getParameter('attachment_id');
	if (sysId) {
		var sysAttGR = new GlideRecord('sys_attachment');
		if (sysAttGR.get(sysId)) {
			var versionGR = new GlideRecord(sysAttGR.getDisplayValue('table_name'));
			if (versionGR.get(sysAttGR.getDisplayValue('table_sys_id'))) {
				answer.versionId = versionGR.getUniqueValue();
				var applicationGR = versionGR.member_application.getRefRecord();
				var sysAppGR = new GlideRecord('sys_app');
				sysAppGR.addQuery('scope', applicationGR.getValue('scope'));
				sysAppGR.query();
				if (sysAppGR.next()) {
					answer.statusMessage = 'Version <strong>' + versionGR.getDisplayValue('version') + '</strong> of application <strong>' + applicationGR.getDisplayValue('name') + '</strong> installed.';
					applicationGR.application = sysAppGR.getUniqueValue();
					applicationGR.update();
					versionGR.installed = true;
					versionGR.update();
					versionGR.initialize();
					versionGR.addQuery('member_application', applicationGR.getUniqueValue());
					versionGR.addQuery('sys_id', '!=', answer.versionId);
					versionGR.query();
					while (versionGR.next()) {
						versionGR.installed = false;
						versionGR.update();
					}
					answer.success = true;
				} else {
					answer.error = 'System application record not found for scope ' + applicationGR.getValue('scope');
				}
			} else {
				answer.error = 'Version record not found for sys_id ' + sysAttGR.getDisplayValue('table_sys_id');
			}
		} else {
			answer.error = 'Attachment record not found for sys_id ' + sysId;
		}
	} else {
		answer.error = 'Missing required parameter: attachment_id';
	}
	return JSON.stringify(answer);
}

That takes care of the server-side code; now we have to update the Client script in our UI Page to make the GlideAjax call to the function and then return the operator to the original version record where the Install button was first selected. At the end of the Commit process, we referenced an updateStoreData function, but did not create it. We will need to create this function now, and then we can make the call within that new function. Before we do that, though, I wanted to note a slight modification that I made to the HTML for that page to add an id tag to the H4 element that includes our status message. The reason that I wanted to do that was so that the final message would not only replace the wording, but it would also replace the loading image that indicated that there was an ongoing process. Once we get to this point, the processing is over, so I did not want to leave that spinning image up on the screen. Here is the modified version of that portion of the HTML:

<h4 style="padding: 30px;" id="final_status_text">
  &#160;
  <img src="/images/loading_anim4.gif" height="18" width="18"/>
  &#160;
  <span id="status_text">Previewing Uploaded Update Set ...</span>
</h4>

Now we can create our new updateStoreData function.

function updateStoreData() {
	document.getElementById('status_text').innerHTML = 'Updating Collaboration Store Database ...';
	var ga = new GlideAjax('ApplicationInstaller');
	ga.addParam('sysparm_name', 'recordInstallation');
	ga.addParam('attachment_id', attachmentId);
	ga.getXMLAnswer(finalizeInstallation);
}

Now we have referenced a finalizeInstallation function, so we will need to create that as well. This function simply adds that final status message to the page and then returns the operator back to the original version form page.

function finalizeInstallation(answer) {
	var result = JSON.parse(answer);
	document.getElementById('final_status_text').innerHTML = result.statusMessage;
	window.location.href = '/x_11556_col_store_member_application_version.do?sys_id=' + result.versionId;
}

Here is the full Client script for the UI Page from top to bottom.

var dataLossConfirmDialog;
var attachmentId = '';
var updateSetId = '';
var commitInProgress = false;

function onLoad() {
	attachmentId = document.getElementById('attachment_id').value;
	updateSetId = document.getElementById('remote_update_set_id').value;
	if (updateSetId) {
		previewRemoteUpdateSet();
	}
}

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

function previewRemoteUpdateSet() {
	var MESSAGE_KEY_DIALOG_TITLE = "Update Set Preview";
	var MESSAGE_KEY_CLOSE_BUTTON = "Close";
	var MESSAGE_KEY_CANCEL_BUTTON = "Cancel";
	var MESSAGE_KEY_CONFIRMATION = "Confirmation";
	var MESSAGE_KEY_CANCEL_CONFIRM_DIALOG_TILE = "Are you sure you want to cancel this update set preview?";
	var map = new GwtMessage().getMessages([MESSAGE_KEY_DIALOG_TITLE, MESSAGE_KEY_CLOSE_BUTTON, MESSAGE_KEY_CANCEL_BUTTON, MESSAGE_KEY_CONFIRMATION, MESSAGE_KEY_CANCEL_CONFIRM_DIALOG_TILE]);
	var dialogClass = window.GlideModal ? GlideModal : GlideDialogWindow;
	var dd = new dialogClass("hierarchical_progress_viewer", false, "40em", "10.5em");

	dd.setTitle(map[MESSAGE_KEY_DIALOG_TITLE]);
	dd.setPreference('sysparm_ajax_processor', 'UpdateSetPreviewAjax');
	dd.setPreference('sysparm_ajax_processor_function', 'preview');
	dd.setPreference('sysparm_ajax_processor_sys_id', updateSetId);
	dd.setPreference('sysparm_renderer_expanded_levels', '0');
	dd.setPreference('sysparm_renderer_hide_drill_down', true);
	dd.setPreference('focusTrap', true);
	dd.setPreference('sysparm_button_close', map["Close"]);
	dd.on("executionStarted", function(response) {
		var trackerId = response.responseXML.documentElement.getAttribute("answer");

		var cancelBtn = new Element("button", {
			'id': 'sysparm_button_cancel',
			'type': 'button',
			'class': 'btn btn-default',
			'style': 'margin-left: 5px; float:right;'
		}).update(map[MESSAGE_KEY_CANCEL_BUTTON]);

		cancelBtn.onclick = function() {
			var dialog = new GlideModal('glide_modal_confirm', true, 300);
			dialog.setTitle(map[MESSAGE_KEY_CONFIRMATION]);
			dialog.setPreference('body', map[MESSAGE_KEY_CANCEL_CONFIRM_DIALOG_TILE]);
			dialog.setPreference('focusTrap', true);
			dialog.setPreference('callbackParam', trackerId);
			dialog.setPreference('defaultButton', 'ok_button');
			dialog.setPreference('onPromptComplete', function(param) {
				var cancelBtn2 = $("sysparm_button_cancel");
				if (cancelBtn2)
					cancelBtn2.disable();
				var ajaxHelper = new GlideAjax('UpdateSetPreviewAjax');
				ajaxHelper.addParam('sysparm_ajax_processor_function', 'cancelPreview');
				ajaxHelper.addParam('sysparm_ajax_processor_tracker_id', param);
				ajaxHelper.getXMLAnswer(_handleCancelPreviewResponse);
			});
			dialog.render();
			dialog.on("bodyrendered", function() {
				var okBtn = $("ok_button");
				if (okBtn) {
					okBtn.className += " btn-destructive";
				}
			});
		};

		var _handleCancelPreviewResponse = function(answer) {
			var cancelBtn = $("sysparm_button_cancel");
			if (cancelBtn)
				cancelBtn.remove();
		};

		var buttonsPanel = $("buttonsPanel");
		if (buttonsPanel)
			buttonsPanel.appendChild(cancelBtn);
	});

	dd.on("executionComplete", function(trackerObj) {
		dd.destroy();
		checkPreviewResults();
	});
	
	dd.render();
}

function checkPreviewResults() {
	document.getElementById('status_text').innerHTML = 'Evaluating Preview Results ...';
	var ga = new GlideAjax('ApplicationInstaller');
	ga.addParam('sysparm_name', 'evaluatePreview');
	ga.addParam('remote_update_set_id', updateSetId);
	ga.getXMLAnswer(commitUpdateSet);
}

function commitUpdateSet(answer) {
	var result = JSON.parse(answer);
	var message = '';
	if (result.accepted > 0) {
		if (result.accepted > 1) {
			message += result.accepted + ' Flagged Updates Accepted; ';
		} else {
			message += 'One Flagged Update Accepted; ';
		}
	}
	if (result.skipped > 0) {
		if (result.skipped > 1) {
			message += result.skipped + ' Flagged Updates Skipped; ';
		} else {
			message += 'One Flagged Update Skipped; ';
		}
	}
	message += 'Committing Update Set ...';
	document.getElementById('status_text').innerHTML = message;
	commitRemoteUpdateSet();
}

function commitRemoteUpdateSet() {
	if (commitInProgress) {
		return;
	}

	var ajaxHelper = new GlideAjax('com.glide.update.UpdateSetCommitAjaxProcessor');
	ajaxHelper.addParam('sysparm_type', 'validateCommitRemoteUpdateSet');
	ajaxHelper.addParam('sysparm_remote_updateset_sys_id', updateSetId);
	ajaxHelper.getXMLAnswer(getValidateCommitUpdateSetResponse);
}

function getValidateCommitUpdateSetResponse(answer) {
	try {
		if (answer == null) {
			console.log('validateCommitRemoteUpdateSet answer was null');
			return;
		}
		console.log('validateCommitRemoteUpdateSet answer was ' + answer);
		var returnedInfo = answer.split(';');
		var sysId = returnedInfo[0];
		var encodedQuery = returnedInfo[1];
		var delObjList = returnedInfo[2];
		if (delObjList !== "NONE") {
			console.log('showing data loss confirm dialog');
			showDataLossConfirmDialog(sysId, delObjList, encodedQuery);
		} else {
			console.log('skipping data loss confirm dialog');
			runTheCommit(sysId);
		}
	} catch (e) {
		console.log(e);
	}
}

function runTheCommit(sysId) {
	console.log('running commit on ' + sysId);
	commitInProgress = true;
	var ajaxHelper = new GlideAjax('com.glide.update.UpdateSetCommitAjaxProcessor');
	ajaxHelper.addParam('sysparm_type', 'commitRemoteUpdateSet');
	ajaxHelper.addParam('sysparm_remote_updateset_sys_id', sysId);
	ajaxHelper.getXMLAnswer(getCommitRemoteUpdateSetResponse);
}

function destroyDialog() {
	dataLossConfirmDialog.destroy();
}

function showDataLossConfirmDialog(sysId, delObjList, encodedQuery) {
	var dialogClass = typeof GlideModal != 'undefined' ? GlideModal : GlideDialogWindow;
	var dlg = new dialogClass('update_set_data_loss_commit_confirm');
	dataLossConfirmDialog = dlg;
	dlg.setTitle('Confirm Data Loss');
	if(delObjList == null) {
		dlg.setWidth(300);
	} else {
		dlg.setWidth(450);
	}
	dlg.setPreference('sysparm_sys_id', sysId);
	dlg.setPreference('sysparm_encodedQuery', encodedQuery);
	dlg.setPreference('sysparm_del_obj_list', delObjList);
	console.log('rendering data loss confirm dialog');
	dlg.render();
}

function getCommitRemoteUpdateSetResponse(answer) {
	try {
		if (answer == null) {
			return;
		}
		var map = new GwtMessage().getMessages(["Close", "Cancel", "Are you sure you want to cancel this update set?", "Update Set Commit", "Go to Subscription Management"]);
		var returnedIds = answer.split(',');
		var workerId = returnedIds[0];
		var sysId = returnedIds[1];
		var shouldRefreshNav = returnedIds[2];
		var shouldRefreshApps = returnedIds[3];
		var dialogClass = window.GlideModal ? GlideModal : GlideDialogWindow;
		var dd = new dialogClass("hierarchical_progress_viewer", false, "40em", "10.5em");
		dd.setTitle(map["Update Set Commit"]);
		dd.setPreference('sysparm_renderer_execution_id', workerId);
		dd.setPreference('sysparm_renderer_expanded_levels', '0');
		dd.setPreference('sysparm_renderer_hide_drill_down', true);
		dd.setPreference('sysparm_button_subscription', map["Go to Subscription Management"]);
		dd.setPreference('sysparm_button_close', map["Close"]);
		dd.on("bodyrendered", function(trackerObj) {
			var buttonsPanel = $("buttonsPanel");
			var table = new Element("table", {cellpadding: 0, cellspacing: 0, width : "100%"});
			buttonsCell = table.appendChild(new Element("tr")).appendChild(new Element("td"));
			buttonsCell.align = "right";
			buttonsPanel.appendChild(table);
			var closeBtn = $("sysparm_button_close");
			if (closeBtn) {
				closeBtn.disable();
			}
			var cancelBtn = new Element("button");
			cancelBtn.id = "sysparm_button_cancel";
			cancelBtn.type = "button";
			cancelBtn.innerHTML = map["Cancel"];
			cancelBtn.onclick = function() {
				var response = confirm(map["Are you sure you want to cancel this update set?"]);
				if (response != true) {
					return;
				}
				var ajaxHelper = new GlideAjax('UpdateSetCommitAjax');
				ajaxHelper.addParam('sysparm_type', 'cancelRemoteUpdateSet');
				ajaxHelper.addParam('sysparm_worker_id', workerId);
				ajaxHelper.getXMLAnswer(getCancelRemoteUpdateSetResponse);
			};
			buttonsCell.appendChild(cancelBtn);
		});

		dd.on("executionComplete", function(trackerObj) {
			dd.destroy();
			updateStoreData();
		});

		dd.render();
	} catch (e) {
		console.log(e);
	}
}

function getCancelRemoteUpdateSetResponse(answer) {
	if (answer == null) {
		return;
	}
}

function updateStoreData() {
	document.getElementById('status_text').innerHTML = 'Updating Collaboration Store Database ...';
	var ga = new GlideAjax('ApplicationInstaller');
	ga.addParam('sysparm_name', 'recordInstallation');
	ga.addParam('attachment_id', attachmentId);
	ga.getXMLAnswer(finalizeInstallation);
}

function finalizeInstallation(answer) {
	var result = JSON.parse(answer);
	document.getElementById('final_status_text').innerHTML = result.statusMessage;
	window.location.href = '/x_11556_col_store_member_application_version.do?sys_id=' + result.versionId;
}

That completes the installation process, the third and final major component of the Collaboration Store application. All that is left now is to release a new Update Set to the testers and see what kinds of bugs we can shake out of this thing before we actually produce an official 1.0 version of the app. If you have not had an opportunity to participate in the testing just yet, now might be a good time to jump in and see what you can find. Feedback of any kind is always welcome. We’ll get more into all of that next time out.

Collaboration Store, Part LII

“Long is the road from conception to completion.”
Molière

Last time, we finished up the Update Set Preview process and it looked like all that was left was to code out the Commit process and we would be done with the last major component of this long drawn-out project. Unfortunately, that’s not entirely true. Before we can move on to the Commit process, we have to deal with the fact that the Preview process may have uncovered some issues with the Update Set. In the manual process, these issues are reported to the operator, and the operator is required to deal them all before the Commit option is available. Not only do we need to address that possibility, we also have to add code to update the application and version records to reflect the version that was just installed and to link the newly installed application with the application record. So we have a little more work to do beyond just launching the Commit process before we can declare project completion.

First of all, we need to decide what to do with any Preview issues that may have been detected. Ideally, you would want to give the operator the opportunity to review these issues and make the appropriate decisions based on their knowledge of their instance and the application. However, since we are trying to make this first version as automated as possible, I have decided to have the software make arbitrary decisions about each reported problem, at least for now. In some future version, I may want to pop up a dialog and ask the operator whether they want to do their own review or trust the system to do it for them, but for now, that’s a little more sophisticated than I am ready to tackle. This may not be the best approach, but it is the simplest, and I am trying wrap up the work on this initial version.

My plan is to add yet another client-callable function to our existing ApplicationInstaller Script Include that will hunt down all of the problems and resolve them. The problem records have a field called available_actions that contains a list of all of the actions available for the problem, so I am going to use that as a guide to Accept Remote Update if I can, or Skip Remote Update if I cannot. I also want to keep track of the number of problems found, the number of updates accepted, and the number of updates skipped so that I can report that information back to the caller. In reviewing the code behind the UI Actions that accept and skip updates, I found a call to a global component called GlidePreviewProblemAction, but when I tried to access that component in my scoped Script Include, I got a security violation error. To work around that, I had to add the following new function to our global utilities, where I could make the call without error.

fixRemoteUpdateIssue: function(remUpdGR) {
	var resolution = 'accepted';
	var ppa = new GlidePreviewProblemAction(gs.action, remUpdGR);
	if (remUpdGR.available_actions.contains('43d7d01a97b00100f309124eda2975e4')) {
		ppa.ignoreProblem();
	} else {
		ppa.skipUpdate();
		resolution = 'skipped';
	}
	return resolution;
}

With that out of the way, I was able to put the rest of the code where it belonged, and just called out to the global component for the part that I was unable to do in the scoped component.

evaluatePreview: function() {
	var answer = {problems: 0, accepted: 0, skipped: 0};
	var sysId = this.getParameter('remote_update_set_id');
	if (sysId) {
		problemId = [];
		var remUpdGR = new GlideRecord('sys_update_preview_problem');
		remUpdGR.addQuery('remote_update_set', sysId);
		remUpdGR.query();
		while (remUpdGR.next()) {
			problemId.push(remUpdGR.getUniqueValue());
			answer.problems++;
		}
		var csgu = new global.CollaborationStoreGlobalUtils();
		for (var i=0; i<problemId.length; i++) {
			remUpdGR.get(problemId[i]);
			var resolution = csgu.fixRemoteUpdateIssue(remUpdGR);
			if (resolution == 'accepted') {
				answer.accepted++;
			} else {
				answer.skipped++;
			}
		}
	}
	return JSON.stringify(answer);
}

Now we just need make the GlideAjax call to that function from the client side before we attempt to launch the Commit process. Right now, when the Preview process is complete, a Close button appears on the progress dialog, and when you click on the Close button, our new UI Page reloads and starts all over again because the script that we lifted from the UI Action on the Update Set form was set up to reload that form. For our purposes, we do not want our own page reloaded, and in fact, we don’t even want a Close button; we just want to move on to the process of reviewing the results of the Preview. The relevant portion of the script that we stole looks like this:

dd.on("executionComplete", function(trackerObj) {
	var cancelBtn = $("sysparm_button_cancel");
	if (cancelBtn)
		cancelBtn.remove();
         
	var closeBtn = $("sysparm_button_close");
	if (closeBtn) {
		closeBtn.onclick = function() {
			dd.destroy();
		};
	}
});
     
dd.on("beforeclose", function() {
	reloadWindow(window);
});

Since we do not want to wait for operator action, we can short-cut this entire operation and just move on as soon as execution has been completed. I replaced all of the above with the following:

dd.on("executionComplete", function(trackerObj) {
	dd.destroy();
	checkPreviewResults();
});

Since the Preview process is now complete at this point, and we are now looking at the results, I decided to wrap the original message on the page with a span that had an id attribute so that I could change the message as things moved along. That line of HTML now looks like this:

<span id="status_text">Previewing Uploaded Update Set ...</span>

With that in place, I was able to update the message with the new status before I made the Ajax call to our new Script Include function.

function checkPreviewResults() {
	document.getElementById('status_text').innerHTML = 'Evaluating Preview Results ...';
	var ga = new GlideAjax('ApplicationInstaller');
	ga.addParam('sysparm_name', 'evaluatePreview');
	ga.addParam('remote_update_set_id', updateSetId);
	ga.getXMLAnswer(commitUpdateSet);
}

function commitUpdateSet(answer) {
	alert(answer);
}

I’m not ready to take on the Commit process just yet, so I stubbed out the commitUpdateSet function with a simple alert of the response from our Ajax call. That was enough to let me know that everything was working up to this point, which is what I needed to know before I attempted to move on.

Now that we have dealt with the possibility of Preview problems, we can finally take a look at what it will take to Commit the Update Set. That’s obviously a bit of work, so we’ll leave all of that for our next episode.

Collaboration Store, Part XLIX

“Don’t tell me the sky’s the limit when there are footprints on the moon.”
Paul Brandt

Last time, we got started on the global UI Script that will run on the upload.do page to take over the page and repurpose it for our needs. Our interest is to convert an Update Set XML file back into an actual Update Set so that we can apply the Update Set, installing a shared Scoped Application. The upload.do page will help set us on that path, but we need our script to implement just a few little modifications. We got as far as launching the GlideAjax process which will fetch the Update Set XML file details from the server side, and now we need to build the function that will process the results coming back and do something with them. The “answer” returned will be a JSON string, so we just need to turn that back into an object so that we can extract the values. We can do just that much and verify the results by popping an alert using one of the values that should be found in the resulting object, the name of the XML file.

function submitForm(answer) {
	var app = {};
	try {
		app = JSON.parse(answer);
	} catch (e) {
		alert('Error parsing JSON response from server: ' + e);
	}
	alert(app.fileName);
}

There is not much here, but we can push the old Install button on the version page, just to verify that all is well so far.

Verification of the server side code, Ajax call, and JSON parsing

Although that wasn’t much in the way of code, it did verify that the server side Script Include that we built a while back does seem to work, as well as the Ajax call that we built last time and the JSON parsing that we just added today. At this point, we have built a UI Action that sends us over to the upload.do page, taken over the page for our own purposes, hiding the original content and adding content of our own, called back to the server side for the XML file information, and demonstrated that the XML file information has indeed been transferred over to the client side. Now that we have it in hand, we have to use it to emulate a file on the local system and send that faux file back over to the server side as an element of a form post. This is where things get a little tricky.

While digging around trying to find a way to do this, I came across the DataTransfer object. This object contains a list of File objects, and you can add to the list using the add() method of the items property. These two lines of code create a new DataTransfer object and add a new file to the empty list using the data that we retrieved from the Ajax call.

var fileList = new DataTransfer();
fileList.items.add(new File([app.xml], app.fileName, {type: 'application/xml'}));

Now that we have our “file” in a file list, we can populate the files attribute of the input element using the files attribute of our DataTransfer object.

document.getElementById('attachFile').files = fileList.files;

Now we just have to submit the form and see what happens. Actually, I did that, and nothing happened. It seems that there are a couple of other form fields that also need to be valued. What seems weird to me is that, if you look at the source code for the page, those fields do start out with a value, but somewhere along the line those values were removed before the form was posted, so I had to add a couple more lines to put those values back.

document.getElementsByName('sysparm_referring_url')[0].value = 'sys_remote_update_set_list.do?sysparm_fixed_query=sys_class_name=sys_remote_update_set';
document.getElementsByName('sysparm_target')[0].value = 'sys_remote_update_set';

Now we can submit the form, which is just one more line of code.

document.forms[0].submit();

All together, our new submitForm function looks like this:

function submitForm(answer) {
	var app = {};
	try {
		app = JSON.parse(answer);
	} catch (e) {
		alert('Error parsing JSON response from server: ' + e);
	}
	var fileList = new DataTransfer();
	fileList.items.add(new File([app.xml], app.fileName, {type: 'application/xml'}));
	document.getElementById('attachFile').files = fileList.files;
	document.getElementsByName('sysparm_referring_url')[0].value = 'sys_remote_update_set_list.do?sysparm_fixed_query=sys_class_name=sys_remote_update_set';
	document.getElementsByName('sysparm_target')[0].value = 'sys_remote_update_set';
	document.forms[0].submit();
}

And that completes (for now) our new global UI Script. Here is the entire script, including all of the work that we did last time out.

if (window.location.pathname == '/upload.do' && window.location.search.startsWith('?attachment_id=')) {
	waitForPageLoad();
}

function waitForPageLoad() {
	if (document.getElementById('attachFile')) {
		installApplication();
	} else {
		setTimeout(waitForPageLoad, 100);
	}
}

function installApplication() {
	var originalContent = document.getElementsByClassName('section-content')[0];
	originalContent.style.visibility = 'hidden';
	var newContent = document.createElement('div');
	newContent.innerHTML = '<h4 style="padding: 30px;">&nbsp;<img src="/images/loading_anim4.gif" height="18" width="18">&nbsp;Uploading Update Set XML file ...</h4>';
	originalContent.parentNode.insertBefore(newContent, originalContent);
	var attachmentId = window.location.search.substring(15);
	var ga = new GlideAjax('x_11556_col_store.ApplicationInstaller');
	ga.addParam('sysparm_name', 'getXML');
	ga.addParam('attachment_id', attachmentId);
	ga.getXMLAnswer(submitForm);
}

function submitForm(answer) {
	var app = {};
	try {
		app = JSON.parse(answer);
	} catch (e) {
		alert('Error parsing JSON response from server: ' + e);
	}
	var fileList = new DataTransfer();
	fileList.items.add(new File([app.xml], app.fileName, {type: 'application/xml'}));
	document.getElementById('attachFile').files = fileList.files;
	document.getElementsByName('sysparm_referring_url')[0].value = 'sys_remote_update_set_list.do?sysparm_fixed_query=sys_class_name=sys_remote_update_set';
	document.getElementsByName('sysparm_target')[0].value = 'sys_remote_update_set';
	document.forms[0].submit();
}

At this point, all that we have accomplished is to load the Update Set. We still have not installed anything. The Update Set still has to be Previewed and then Committed before the version is actually installed. The ultimate goal will be for the operator to be able to just click on that Install button and have everything else takes care of itself, including marking the version record as Installed (and any other version records of the app as not installed). Whether or not we can do all of that without human intervention has yet to be determined, but we have at least accomplished that first step of turning the XML file back into an Update Set. Next time, we will see where we can go from here.

Collaboration Store, Part XLVIII

“Most times, the way isn’t clear, but you want to start anyway. It is in starting that other steps become clearer.”
Israelmore Ayivor

Last time, we created a process to retrieve the Update Set XML data from the server side and then built a UI Action to launch the installation process. At the time that we left off, I was vacillating back and forth between hacking up the original upload.do page and creating a customized copy of my own. Since that time, though, I have decided that I am much too lazy to try to build one of my own, so I am just going to attempt to hack up the one that already exists with as minimal amount of intervention as I can muster. The one way that I know how to do that is to create a global UI Script that modifies the page on the fly without actually altering the source of the page itself. We have already used this technique with our earlier incident email hack, so at least we know that this approach is one that will work.

Unfortunately, you cannot create global UI Scripts in a Scoped Application; the script has to be in the global scope, so this component will be yet another addition to our global components Update Set. I don’t really like having all of these parts outside of the application, but that’s just the way that these things go sometimes. These global scripts run on every single page load in the system, so to be a minimally intrusive as possible, the very first thing that you want to check is whether or not you are running on a page in which this code is needed. For our purposes, we only want this code to run on the upload.do page, and only if our attachment_id parameter is present in the URL.

if (window.location.pathname == '/upload.do' && window.location.search.startsWith('?attachment_id=')) {
	alert('So far, so good ...');
}

We can test this out by going into a version record and clicking on the new Install form button.

First test of the new global UI Script

OK, that works. In fact, that also proves out the code on the UI Action that we created last time. As the alert says, so far, so good. One thing that you will notice, however, is that there is nothing on the underlying screen. This code runs as soon as it is loaded, and the rest of the page has yet to be delivered. Since our plan is to tinker with that page, we really don’t want our code to be running just yet. We will need to wait to make sure that the rest of the page is there as well before we attempt to alter it. We can accomplish that with a little recursive loop that will look for an important field such as the file to be uploaded, and until that element is present, just loop back and check again. Here is a modified version of the script that will accomplish that.

if (window.location.pathname == '/upload.do' && window.location.search.startsWith('?attachment_id=')) {
	waitForPageLoad();
}

function waitForPageLoad() {
	if (document.getElementById('attachFile')) {
		installApplication();
	} else {
		setTimeout(waitForPageLoad, 100);
	}
}

function installApplication() {
	alert('So far, so good ...');
}

If that works as intended, the alert should not pop until at least the parts of the page in which we are interested have arrived.

Second test of the new global UI Script

That’s better. Now at least the stuff that we want to play with is all present in the DOM. The first thing that we will want to do is to hide the original form and then replace it with some kind of message indicating that things are happening in the background and there is nothing for the operator to do right at the moment. Here is a little code that will find the DIV that contains the major components, hides it, and replaces it with something else.

var originalContent = document.getElementsByClassName('section-content')[0];
originalContent.style.visibility = 'hidden';
var newContent = document.createElement('div');
newContent.innerHTML = '<h4 style="padding: 30px;">&nbsp;<img src="/images/loading_anim4.gif" height="18" width="18">&nbsp;Uploading Update Set XML file ...</h4>';
originalContent.parentNode.insertBefore(newContent, originalContent);

There are a couple of things to note on the above code. For one, DOM manipulation is frowned upon in the ServiceNow environment. You will get tagged for that in an Instance Scan as a bad practice, and you should really try to avoid doing things like that if at all possible. Still, sometimes you have to break the rules to get something done; there is a reason that this site is called ServiceNow Hackery and not ServiceNow By The Book. Sometimes you have to step outside of the lines in order to do what you want to do. But again, this should be a last resort and not adopted as a routine way of doing things. The other thing to note is the use of the innerHTML method. Again, the preferred way of doing things would be to create each DOM node individually, set all of the appropriate values on each node, and then link them all up to each other before inserting them into the active DOM. That’s the way that it should be done, but I was just too lazy to go through all of that and I took the easy way out instead. But that’s another thing to which folks might take exception in certain circles.

To test all of this out, we can go back to our version page and click on the new Install button one more time.

Third test of the new global UI Script

With all of that basic housekeeping out of the way, we can now focus on what we are here for. The first thing that we need to do in order to accomplish our goal is to pull down the Update Set details using GlideAjax to access the Script Include that we created last time. Before we do that, though, we need to snag the attachment record sys_id from the URL parameter. With that in hand, we can then make our Ajax call.

var attachmentId = window.location.search.substring(15);
var ga = new GlideAjax('x_11556_col_store.ApplicationInstaller');
ga.addParam('sysparm_name', 'getXML');
ga.addParam('attachment_id', attachmentId);
ga.getXMLAnswer(submitForm);

Now we just need to build a submitForm function that will parse the returned JSON string to access the file name and file contents, and then somehow use that as if it were a file on the local system so that we can submit the form. That sounds like a bit of work in and of itself, and I’m still not exactly sure how I am going to pull that off, so let’s save that exercise for our next exciting installment.

Collaboration Store, Part XLVII

“Relinquish your attachment to the known, step into the unknown, and you will step into the field of all possibilities.”
Deepak Chopra

While we wait patiently for the testing results of the instance sync process to come trickling in, we should go ahead and get started on the application installation process. As I laid out earlier, there are a couple of different ways to go here, and I really do not like either one. However, as much as I would like to have come up with a preferable third alternative, nothing has really come to mind, despite the fact that I have been dragging out the instance sync process for much longer than I should have while I searched in vain for a better solution. We have to keep pushing forward, though, so enough waiting. Let’s get into it.

I have decided to go with the client-side approach, mainly because I don’t want to get involved with the security issues related to attempting to emulate a user session on the server side. Both approaches involve quite a bit more hackery than I really care to include in something that is supposed to be a viable product to be distributed and used by others, but at this point, I don’t see any other way. The client-side approach involves downloading the Update Set XML file from the server, and then presenting it back as if it were a local file uploaded to the import XML process. So, to start with, we will need a Script Include function that will deliver the XML to the client side. Since this will be a client callable Script Include, we will want to create a new one rather than add another function to our existing utilities. We will call this new Script Include ApplicationInstaller, and check the Client callable checkbox when we create it. Our lone function will be called getXML and it will accept a single parameter for the sys_id of the attachment record, and then use that sys_id to fetch the record and return both the file name and the file contents in a JSON string response.

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

	getXML: function() {
		var answer = {};
		var sysId = this.getParameter("attachment_id");
		if (sysId) {
			var sysAttGR = new GlideRecord('sys_attachment');
			if (sysAttGR.get(sysId)) {
				var gsa = new GlideSysAttachment();
				answer.fileName = sysAttGR.getDisplayValue('file_name');
				answer.xml = gsa.getContent(sysAttGR);
			}
		}
		return JSON.stringify(answer);
	},

    type: 'ApplicationInstaller'
});

There’s nothing too extraordinary about the code here. We do leverage our old friend the GlideSysAttachment to grab the contents of the file, but that’s nothing that we have not already done before. Other than that, it’s pretty basic stuff.

Now that we have a way to fetch the name and contents of the attached file, we will need a way to launch the process, which will either be a clone of the existing upload.do page or the original upload.do page with a little creative hackery to bend it to our will. I hate to mess with the original, but I also don’t want to create an entire second copy if I don’t have to. But let’s not get too far ahead of ourselves just yet. Right now, we just need to build a launcher for the process, and we can easily change the page that we launch once we decide which way to go.

To launch the installation process, we can add a UI Action to the version form, which we can simply label Install. We will want to make this action conditional, since we would not want to see this option on a version that has already been installed. At this point, we don’t need to worry about the code behind the action; we can just create it and see how it renders out on the page.

New UI Action “Install”

After saving the new action, we can pull up a version page and see how it looks.

New UI Action rendered on the version page

Now we need to add some code to make the action actually do something. Basically, we just want to jump over to whatever version of the upload.do page we end up pursuing, but before we do that we need to include a parameter. The client callable Script Include that we just created takes an attachment record sys_id as a parameter, but out action is sitting on the version record, not the attachment record. We will need to hunt down the attachment record to pull out the sys_id and then we can pass that along to our destination so that it can be used to make the GlideAjax call to our Script Include function. Here is the UI Action script to make all of that happen.

var attachmentGR = new GlideRecord('sys_attachment');
attachmentGR.addQuery('table_name', 'x_11556_col_store_member_application_version');
attachmentGR.addQuery('table_sys_id', current.sys_id);
attachmentGR.addQuery('content_type', 'CONTAINS', 'xml');
attachmentGR.query();
if (attachmentGR.next()) {
	action.setRedirectURL('/upload.do?attachment_id=' + attachmentGR.getUniqueValue());
} else {
	gs.addErrorMessage('No Update Set XML file found attached to this version');
}

Once again, this is pretty basic stuff that doesn’t require a whole lot of explanation. We query the attachment table for an XML file attached to the current record, and if we find it, we construct a URL and jet off to that page. If not, we throw up an error message, but that really shouldn’t happen unless something has gone horribly wrong somewhere along the line.

So now we have built enough parts to get us to the page that will actually fetch the XML from the server and then push it back up again. I need to make a decision as to whether I should hack up the original upload.do page or just use it as a guide to make one of my own. I’ll need to give that some thought, so let’s pause for now and we’ll jump into that next time out.

Collaboration Store, Part XXXVI

“The definition of flexibility is being constantly open to the fact that you might be on the wrong track.”
Brian Tracy

Although it is long past time to start some serious work on the third major component of this little(?) project, the application installation process, I have been rather hesitant to officially kick that off. Last time, we addressed the last of the reported defects in the first two processes, the initial set-up and the application publishing process. Now, it would seem, would be the time to jump into wrestling with that last remaining primary function and put that to bed before turning our attentions to the list of secondary features that will complete the initial release of the full product. At least, that would appear to be the next logical step.

The reason for my reluctance, however, is that I have taken a cursory look at the various approaches available to accomplish my goal, and quite frankly, I don’t really like any of them. When we converted our Update Set to XML, we were able to fish out enough parts and pieces from the stock product to cobble together a reasonable solution with a minimal amount of questionable hackery. To go in the other direction, to convert the XML back to an Update Set, the only stock component that appears to provide this functionality is bound tightly with the process of uploading a local file from the user’s file system. The /upload.do page, or more specifically, the /sys_upload.do process to which the form on that page is posted, handles both the importing of the XML file and the conversion of that file to an Update Set. There is no way to extract the process that turns the XML into an Update Set, since everything is encapsulated into that one all-encompassing process. For our purposes, we do not have a local file on the user’s machine to upload; our file is an attachment already present on the server, so invoking this process, which seems the only way to go, involves much more than we really need.

To invoke the one and only process that I have found (so far!) to make this conversion, then, we will have to post a multi-part form to /sys_upload.do that includes our XML data along with all of the other fields, headers, and cookies expected by the server-side form processor. On the server side, we should be able to accomplish this with an outbound REST message, or on the client side, we should be able to do this by somehow sending our XML instead of a local file when submitting the form. Each approach has its own merits, but they also each have their own issues, and no matter which way you go, it’s a rather complicated mess.

The Server Side Approach

Posting a multi-part form on the server side is actually relatively simple as far as the form data goes. We can construct a valid body for a standard multipart/form-data POST using our XML data and related information and then send it out using an outbound REST message. That’s the easy part. We just need to add an appropriate Content-Type header, including some random boundary value:

Content-Type: multipart/form-data; boundary=somerandomvalue

Then we can build up the body by including all of the hidden fields on the form, and then add our XML in a file segment that would look something like this:

------------somerandomvalue
Content-Disposition: form-data; name="attachFile"; filename="app_name_v1.0.xml"
Content-Type: application/xml

<... insert XML data here ...>

------------somerandomvalue--

In addition to the XML file component, you would also need to send all of the other expected form field values, some of which are preloaded on the form when it is delivered. To obtain those values, you would have to first issue an HTTP GET request of the /upload.do page and pick those values out of the resulting HTML. This can be accomplished with a little regex magic and the Javascript string .match() method. Here is a simple function to which you can pass the HTML and a pattern to return the value found in the HTML based on the pattern:

function extractValue(html, pattern) {
	var response = '';
	var result = html.match(pattern);
	if (result.length > 1) {
		response = result[1];
	}
	return response;
}

For example, one of the form fields found on the /upload.do page is sysparm_ck. The INPUT element for this hidden field looks like this:

<input name="sysparm_ck" id="sysparm_ck" type="hidden" value="68fa4eee2fa401104425fcecf699b646939f52c6787c23fff22b124fcf58f713235b7478"></input>

To snag the value of that field, you would just pass the HTML for the page and the following pattern to our extractValue function:

id="sysparm_ck" type="hidden" value="(*.?)"

Once you have obtained the value, you can use it to build another “part” in the multi-part form body:

------------somerandomvalue
Content-Disposition: form-data; name="sysparm_ck"

68fa4eee2fa401104425fcecf699b646939f52c6787c23fff22b124fcf58f713235b7478

------------somerandomvalue

All of that is pretty easy to do, and would work great except for one thing: this POST would only be accepted as part of an authenticated session, and cannot just be sent in on its own. Theoretically, we could create an authenticated session by doing a GET of the /login.do page and then a POST of some authoritative user’s credentials, but that would mean knowing and sending the username and password of a powerful user, which is a dangerous thing with which to start getting involved. For that reason, and that reason alone, this does not seem to be a good way to go.

The Client Side Approach

On the client side, you are already involved in an authenticated session, so that’s not any kind of an issue at all. What you do not have is the XML, so to do anything on the client side, we will first need to create some kind of GlideAjax service that will deliver the XML over to the client. Once we have the XML that we would like to upload in place of the normal local file, we will have to perform some kind of magic trick to update the form on the page with our data in the place of a file from the local computer. To do that, we will have to either create our own copy of the /upload.do page or add a global script that will only run on that page, and only if there is some kind of URL parameter indicating that this is one of our processes and not just a normal user-initiated upload. We did this once before with a global script that only ran on the email client, so I know that I can run a conditional script on a stock page if I do not create a page of my own, but the trick will be getting the XML data to be sent back to the server with the rest of the input form.

After nosing around a bit for available options, it appears that you might be able to leverage the DataTransfer object to build a fake file, link it to the form, and then submit the form using something like this:

function uploadXML(xml, fileName) {
	var fileList = new DataTransfer();
	fileList.items.add(new File(xml, fileName));
	document.getElementById('attachFile').files = fileList.files;
	document.forms[0].submit();
}

Of course, there will be a lot more to it than that, as you will need to get a handle on the Update Set created and then try to Preview and Commit it programmatically as well, but this looks like a possibility. Still, you have to move the entire Update Set XML all the way down to the client just to push it all the way back up to the server again, which seems like quite a waste. Plus, with any client-side functionality, there is always the browser compatibility issues that would all need to be tested and resolved. Maybe this would work, but I still don’t like it. It seems like quite a bit of complexity and more than a few opportunities for things to go South. I’m still holding out hope that there is a better way.

Now what?

So … given that I don’t like any of the choices that I have come up with so far, I have decided to set that particular task aside for now in the hopes that a better alternative will come to me before I invest too much effort into a solution with which I am not all that thrilled. There is no shortage of things to do here, so my plan is to just focus on other issues and then circle back to this particular effort when a better idea reveals itself, or I run out of other things to do. Technically, once you have obtained the XML for a particular version from the Host, you can still manually install it by downloading the attachment yourself and importing like any other XML Update Set. That’s not really how I intend all of this to function, but it does work, so it should be OK to set this aside for a time.

Next time, then, instead of forging ahead with this third major component as I had originally planned, I will pick something else out of the pile and we will dig into that instead.

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.

Fun with Outbound REST Events, Part VI

“Quality is never an accident. It is always the result of intelligent effort.”
John Ruskin

Now that we have completed our address verification feature, we can finally turn our full and complete attention to the actual purpose of this entire adventure, which which is to explore the use of ServiceNow Event Management practices on the internal workings of ServiceNow itself. When we last left our Script Include, we had identified a number of places in the script where things could potentially go wrong. As a temporary measure, we just put a simple gs.info statement in each one of those places. Now we want to replace those with Event logging so that we can leverage the built-in power of the ServiceNow Event Management infrastructure.

To make that easier, we built a utility a while back to handle much of the heavy lifting of logging an Event. We can take advantage of that utility and minimize the code that we will need to our Script Include. Each gs.info statement will need to be replaced with something like this:

var seu = new ServerEventUtil();
seu.logEvent(source, resource, metric_name, severity, description, additional_info);

Now we just need to figure out what values to send for each of those function arguments. Let’s take them one at a time.

source

This is the source of the Event, which is our case is the Script Include that is logging the Event. Since the name of the Script Include is always stored in an internal property called type, I just like to pass this.type for this argument, which works in all Script Includes without modification.

resource

This is a reference to thing that you were working on when the problem occurred. In our case, this would be a User, but in the current configuration, we do not have a handle on the User record that is being updated. We could use the address here, just to have some kind of unique value, but when we turn this Event into an Incident, it would be good to know which User was being updated. The solution to that would be to have the calling script pass some reference to User record as an additional argument to the function. That’s a little more work, but it will be worth it in the long run.

metric_name

This is basically the problem that occurred, and we will end up with a different value here for different issues such as an unparsable JSON string or a bad HTTP Response Code.

severity

This is just your standard severity values, and for our purposes, I think we will just pass a hard-coded 3 (Moderate) here.

description

As the name implies, this is just a text description of what happened. Ours will be unique to the problem that occurred.

additional_info

This is an open-ended JSON object into which you can stuff basically anything that you might want to know about what happened that isn’t already in a defined property. The Event logging utility automatically adds some standard things to this object such as user information and a stack trace, but we will want to add some additional information as well such as what was sent to the service and what came back. It takes a bit of code to construct the additional info object, so I like to build a function for that so that it can be called from wherever it is needed instead of duplicating the code everywhere. Here is the one that we will add for this exercise:

buildAdditionalInfo: function(input, response, respObject, exception) {
	var additionalInfo = {input: {}, response: {}};

	additionalInfo.input.street = input.street;
	additionalInfo.input.city = input.city;
	additionalInfo.input.state = input.state;
	additionalInfo.input.zip = input.zip;
	additionalInfo.response.code = response.getStatusCode();
	additionalInfo.response.content = response.getBody();
	additionalInfo.response.headers = response.getHeaders();
	if (respObject) {
		additionalInfo.response.object = respObject;
	}
	if (exception) {
		additionalInfo.exception = exception.toString();
		additionalInfo.stackTrace = exception.stack;
	}

	return additionalInfo;
},

Using a function for this not only consolidates the code into a single place, it also ensures some consistency between the various Events, which makes it easier to pull the data back out when you want to use it for things like formatting the description of a resulting Incident.

Now that know how we are going to populate these arguments, let’s go down through the code and replace each of our gs.info statements with Event logging. The first one that we come across is the JSON parsing exception.

try {
	respArray = JSON.parse(body);
} catch (e) {
	seu.logEvent(
		this.type,
		user,
		'Unparsable response',
		3,
		'The response content received from the US Address validation service could not be parsed.',
		this.buildAdditionalInfo(response, resp, null, e));
}

At this point in the process, we do not have a response object, but we do have an exception, so we pass null as the response object to the function that builds out the additional info. All of the others will be very similar, so we don’t have to go through each one individually. Here is the complete function, with all of the gs.info statements replaced and the user identifier added as a function argument:

validateAddress: function(user, street, city, state, zip) {
	var response = {result: 'failure', street: street, city: city, state: state, zip: zip};

	var seu = new ServerEventUtil();
	var rest = new RESTMessage('US Street Address API', 'get');
	rest.setStringParameter('authid', gs.getProperty('us.address.service.auth.id'));
	rest.setStringParameter('authToken', gs.getProperty('us.address.service.auth.token'));
	rest.setStringParameter('street', encodeURIComponent(street));
	rest.setStringParameter('city', encodeURIComponent(city));
	rest.setStringParameter('state', encodeURIComponent(state));
	rest.setStringParameter('zip', encodeURIComponent(zip));
	var resp = rest.execute();
	var body = resp.getBody();
	if (resp.getStatusCode() == 200) {
		var eventLogged = false;
		var respArray = [];
		try {
			respArray = JSON.parse(body);
		} catch (e) {
			seu.logEvent(
				this.type,
				user,
				'Unparsable response',
				3,
				'The response content received from the US Address validation service could not be parsed.',
				this.buildAdditionalInfo(response, resp, null, e));
			eventLogged = true;
		}
		if (respArray && respArray.length > 0) {
			respObj = respArray[0];
			if (typeof respObj.analysis == 'object') {
				var validity = respObj.analysis.dpv_match_code;
				if (validity == 'Y' || validity == 'S' || validity == 'D') {
					response.result = 'valid';
					response.street = respObj.delivery_line_1;
					response.city = respObj.components.city_name;
					response.state = respObj.components.state_abbreviation;
					response.zip = respObj.components.zipcode;
					if (respObj.components.plus4_code) {
						response.zip += '-' + respObj.components.plus4_code;
					}
				} else {
					response.result = 'invalid';
				}
			} else {
				seu.logEvent(
					this.type,
					user,
					'Invalid Response Object',
					3,
					'The response object received from the US Address validation service was not valid.',
					this.buildAdditionalInfo(response, resp, respObj));
			}
		} else {
			if (!eventLogged) {
				seu.logEvent(
					this.type,
					user,
					'Invalid Response Content',
					3,
					'The response content received from the US Address validation service was not valid.',
					this.buildAdditionalInfo(response, resp));
			}
		}
	} else {
		seu.logEvent(
			this.type,
			user,
			'Invalid Response Code',
			3,
			'The response code received from the US Address validation service was not valid.',
			this.buildAdditionalInfo(response, resp));
	}

	return response;
},

The one place where we had to add a little bit of extra logic was the Event that is triggered when the respArray is empty. One possible reason for that field to be empty would be if we failed to successfully parse the JSON string. When that happens, we have already logged an Event, so we would not want to now log a second one for the same issue. To prevent that from happening, we added the eventLogged variable, and then we only log an Event later on if that variable is still set to false. Other than that one special circumstance, all of these are pretty much the same other than the unique values that are specific to the particular problem triggering the Event.

That completes the modifications necessary to support Event logging, but since we added the user identifier to the list of function arguments, we still have a little work to do to carry that change forward through all of the other components. To begin, we will have to collect the user value from the Ajax parameters and pass that on to the primary function. That client callable function now looks like this:

validateAddressViaClient: function() {
	var user = this.getParameter('sysparm_user');
	var street = this.getParameter('sysparm_street');
	var city = this.getParameter('sysparm_city');
	var state = this.getParameter('sysparm_state');
	var zip = this.getParameter('sysparm_zip');
	return JSON.stringify(this.validateAddress(user, street, city, state, zip));
},

Not much change here; we pull one more parameter into a variable and then add that variable to the function call arguments. Of course, none of that will do any good if we don’t send that extra parameter with the Ajax call, so we will need to modify our Client Script as well. Again, there is not much to change here, but we need to make the change. Our code to value the parameters now has one additional line:

ga.addParam('sysparm_name', 'validateAddressViaClient');
ga.addParam('sysparm_user', g_form.getValue('user_name'));
ga.addParam('sysparm_street', street);
ga.addParam('sysparm_city', city);
ga.addParam('sysparm_state', state);
ga.addParam('sysparm_zip', zip);

That completes the changes that we need to make in order to log an Event whenever something unexpected occurs. We still need to test everything to make sure that it all works, but to do that, we are going to have to force some kind of error to occur. That sounds like a project in and of itself, so this seems like a good stopping place for now. We’ll figure out all of that testing stuff in our next installment.

Fun with Outbound REST Events, Part V

“The best preparation for good work tomorrow is to do good work today.”
Elbert Hubbard

At the end of the last installment in this series, I mentioned two possible options for the next item on which to focus our energies. At the time, I wasn’t really sure which direction would be the preferable choice, but now that it is time to fish or cut bait, a decision needs to be made. Given that the entire purpose of this exercise is to demonstrate the use of Event Management practices on internal ServiceNow processes, I believe it would be good to go ahead and wrap up our example Use Case at this point so that we can devote the remainder of our time exclusively on the Event Management aspects. All that is really left to do in order to to complete our address verification scenario is to add the form validation to the User Profile form. That process will leverage the new Script Include and Outbound REST Message that we have already completed.

There are two different ways that we can go about this: we can use an onSubmit Business Rule on the server side, or we can use an onSubmit Client Script on the client side. The Business Rule route is actually a little easier, as you have access to both the current and previous versions of the GlideRecord, and you can call the Script Include directly, without the need for GlideAjax. My problem with that, though, from a User Experience perspective, is that you have to submit the entire form to the server for processing, which then gets reloaded if you have validation issues. My preference is to validate the form right where it sits on the client side, before the form ever gets submitted to the server. For that, we need to build a Client Script.

In fact, for our purpose, we will need two Client Scripts, one an onLoad script and the other an onSubmit script. The reason that we have to have an onLoad script is because you do not have access to the previous field values on the client side like you do with a server-side Business Rule. We will need to snag those values and stuff them somewhere for safekeeping as soon as the form loads. The ServiceNow platform provides a place for just that sort of thing called the g_scratchpad. You can pretty much throw whatever you want in there and it will be available for use until the form is reloaded. The entire onLoad script is just a few short lines of code.

function onLoad() {
	g_scratchpad.originalStreet = g_form.getValue('street');
	g_scratchpad.originalCity = g_form.getValue('city');
	g_scratchpad.originalState = g_form.getValue('state');
	g_scratchpad.originalZip = g_form.getValue('zip');
}

That’s all there is to that. With those initial values safely tucked away, we can then pull them back out later on and refer to them in our onSubmit script to see if anything has changed. Before we start in on our onSubmit script, however, we need to go back to our Script Include and add just a bit of code. When we first built out Script Include, we did not set it up to be client callable, but we are going to need to do that now so that we can access it via GlideAjax. It’s a simple checkbox on the Script Include form, which triggers a slight change in the prototype for the script. That change is handled automatically for any new script, but since we have been working on ours for a while, checking the box may not actually alter any modified code. In that case, you just need to modify the prototype line yourself to look like this:

AddressValidationUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {

Also, you will need to go down to the very bottom of the script and insert a closing paren in between the final curly brace and the terminating semi-colon. Once that’s done, we can add a new function that we can call from the client side.

validateAddressViaClient: function() {
	var street = this.getParameter('sysparm_street');
	var city = this.getParameter('sysparm_city');
	var state = this.getParameter('sysparm_state');
	var zip = this.getParameter('sysparm_zip');
	return JSON.stringify(this.validateAddress(street, city, state, zip));
},

This function just grabs all of the parameters passed via GlideAjax and passes them to our existing function, and then turns the response object into a string that can be returned in the XML Ajax response. Now we are all set up to receive calls from the client side.

Being a validation script, our onSubmit script will need to prevent the submission of the form in the event that any validation errors are detected. Without an Ajax call back to the server, that’s easily accomplished by returning false from the onSubmit function. However, making an asynchronous Ajax call means you are leaving your function without the answer in hand, so you don’t know whether or not any issues were detected until the response comes back, which will be in a completely different thread. Although you could utilize the getXMLWait() method to simply wait for the answer, that approach is quite frowned upon in Client Scripts for a variety of reasons, so we do not want to go down that road. Instead, we will use a modified version of this technique.

The approach is to cancel submission of the form before making the Ajax call, and then when the response comes in, submit the form a second time if all is well. Of course, submitting the form again will launch the onSubmit script again, so we need to make it smart enough to know that this is the second submit and to not start the whole process all over again. To accomplish that, we use yet another g_scratchpad property to let the script know that validation has already taken place. Here is the main onSubmit script:

function onSubmit() {
	var submitForm = true;

	var actionName = g_form.getActionName();
	if (!g_scratchpad.isFormValid) {
		submitForm = checkAddress();
	}

	return submitForm;
}

On the first submit, we default the submitForm variable to true, capture the name of the button that was pushed to submit the form so that we can use it when we submit again later, and then check to see if validation has already taken place and passed by testing the isFormValid scratchpad property. In the case of the first submit, isFormValid has not been set to true, so we check the address to see if it needs validated. If it does, then the submitForm variable will be set to false, and the form will not be submitted.

Assuming that it does need to be validated, the checkAddress function will make the Ajax call and cancel the original form submission by returning false. When the Ajax response comes in, if the address is valid, the response function will then submit the form a second time using the saved actionName after setting the isFormValid scratchpad property to true. When the onSubmit function then starts again due to the second submit, it will bypass the address check and simply allow the form to submit due to the isFormValid scratchpad property being set to true.

It’s all a little convoluted, but it works. Here is the checkAddress function:

function checkAddress() {
	var submitForm = true;

	var street = g_form.getValue('street');
	var city = g_form.getValue('city');
	var state = g_form.getValue('state');
	var zip = g_form.getValue('zip');
	if (street || city || state || zip) {
		if (street != g_scratchpad.originalStreet || city != g_scratchpad.originalCity || state != g_scratchpad.originalState || zip != g_scratchpad.originalZip) {
			GlideUI.get().clearOutputMessages();
			var ga = new GlideAjax("AddressValidationUtils");
			ga.addParam('sysparm_name', 'validateAddressViaClient');
			ga.addParam('sysparm_street', street);
			ga.addParam('sysparm_city', city);
			ga.addParam('sysparm_state', state);
			ga.addParam('sysparm_zip', zip);
			ga.getXMLAnswer(processXMLAnswer);
			submitForm = false;
		}
	}

	return submitForm;
}

Just like in the main onSubmit function, we first default the submitForm variable to true, and then we grab all of values for the four address components off of the g_form object. The first thing that we check is if there is even any data in any of the four fields. If they are all empty, then there is nothing to validate. If there is some data there, then the next thing that we check is if it is any different than the values that we squirreled away in our onLoad script. If there are no changes, then again there is no need for validation. But if there is data there and it has changed in any way, now we are going to be making that GlideAjax call. Most of that is pretty standard GlideAjax stuff, but we also clear out any error messages on the screen from previous attempts and set the submitForm variable to false to kill the original form submission.

To process the Ajax response, we have yet another function, processXMLAnswer:

function processXMLAnswer(answer) {
	var response = JSON.parse(answer);
	if (response.result == 'invalid') {
		g_form.addErrorMessage('Unable to verify address entered using the US Address validation service');
		g_form.showFieldMsg('street', 'Unverifiable address', 'error');
		g_form.showFieldMsg('city', 'Unverifiable address', 'error');
		g_form.showFieldMsg('state', 'Unverifiable address', 'error');
		g_form.showFieldMsg('zip', 'Unverifiable address', 'error');
	} else {
		if (response.result == 'valid') {
			g_form.setValue('street', response.street);
			g_form.setValue('city', response.city);
			g_form.setValue('state', response.state);
			g_form.setValue('zip', response.zip);
		}
		g_scratchpad.isFormValid = true;
		g_form.submit(actionName);
	}
}

The response comes back in the form of a string, so the first thing that we have to do is convert it back to an object. If the result property of that object is ‘invalid’, then we leave the form unsubmitted and throw a few error messages up on the screen; otherwise, we are going to submit the form a second time. But before we do that, we check to see if the result property is ‘valid’, in which case we overlay the user’s input with the clean address values returned by the service. After that , we set our isFormValid scratchpad property to true and resubmit the form using the saved actionName.

It’s all a little complicated, having to pass through the onSubmit script twice due to the double submit, but it all makes sense if you really think about it. Of course, we won’t really know if it works until we try it, so let’s pull up a user’s profile and give it a shot.

For our first test, let’s see if we can get a validation error. We can use the test address that we have been using up to this point, but let’s change the state from FL to TX and let’s drop the zip code entirely. Also, let’s put everything in lower case, just for fun. OK, let’s see what happens.

Test using invalid address

Having both form-level and field-level error messages is probably a little overkill, but everything seems to have worked. Now, let’s change the state back to FL and see if that is enough to consider it a valid address.

User profile after address validation

Not only did it pass validation, it also corrected our capitalization and added the missing zip code. That’s actually pretty slick. This wasn’t really the point of this series, but I like this address validation feature. I was originally just looking for something that might have a failure to showcase the Event Management stuff, but this turned out to be a pretty handy little addition that could be useful in a number of other places, such as the Building and Location forms.

This now completes the example working feature that has the potential for failure. From this point on, we can focus on what we came here for, which is to log and process Events that originate in ServiceNow. Next time out, we will start in on the logging.