“I believe in a world where all these things can happen, even if I have to do them myself.”
— Adrian Lamo
While we wait for the hordes of fellow developers currently running their extensive tests on the initial version of the Collaboration Store app to report their findings, I thought it might be a good time to give a little bit of thought as to where things should go from here. As I mentioned earlier, there are a number of other improvements that I would like to make to the set-up process once we are sure that everything is working as it should. That would definitely be one specific area towards which we could direct our attentions next. On the other hand, I am quite anxious to see if I can actually do some real sharing of artifacts between instances, and I am not quite sure how I am going to do that, so I would like to start focusing on that little project as well. I have managed to cobble together enough parts to get multiple instances aware of one another, so even though the initial set-up process is not quite ready for prime time, maybe it is good enough for now and I can start trying to work towards accomplishing the actual purpose of this application. Besides, we have not received any results from any of those selfless volunteers who have been doing the outside testing as yet, so we really don’t want to disturb that code at this point.
I would like to share all kinds of different artifacts at some point, but to start out, I am going to focus on sharing Scoped Applications in much the same way that applications are shared to internal app stores or the public ServiceNow App Store. There are two sides to the process of sharing: 1) publishing your app to the desired target, and 2) retrieving an app from the desired source and installing it in your instance. Since I can only do one thing at a time, and one obviously has to take place before you can do the other, my initial focus will be to see if I can publish an app to the Host instance. Once we clear that hurdle, then maybe we can see if we can do the flip side and pull something down from the Host and get it installed. But that’s nothing that we need to worry about today, particularly since we don’t even know yet if we can pull off putting something out there!
The first thing that we will need is a couple of tables, one to store the published applications and another to store each version of the application. These are just meta data tables containing information about the app and the versions. I envision that the application itself will be an XML Update Set file, which I plan to attach to the application version table record for each version published. The question is, How do we do that?
There are already built-in capabilities to turn an app into an Update Set and to turn an Update Set into an XML file. Both of those are UI Actions, so taking a peek under the hood of those artifacts would seem like a good place to start. Before you can turn an Update Set into an XML file, you first have to have an Update Set, so let’s first take a peek at the code under the UI Action that does that. The easiest way to do that would be to pull up a Scoped Application and then use the hamburger menu to pull up the list of UI Actions for that form.
The one that we are looking for is called Publish to Update Set… and what we are looking for is the code that is executed when the action is selected.
function publishIt() {
var sysId = gel('sys_uniqueValue').value;
var dialogClass = window.GlideModal ? GlideModal : GlideDialogWindow;
var dd = new dialogClass("publish_app_dialog");
dd.setTitle(new GwtMessage().getMessage('Publish to Update Set'));
dd.setPreference('sysparm_sys_id', sysId);
dd.setWidth(500);
dd.render();
}
Well, that didn’t tell us much. All this code does is launch a dialog box. We will need to look at the UI Page that will appear in the pop-up window, which we can see from the code is called publish_app_dialog. The key element there is the publishApp() function in the Client script of that UI Page.
function publishApp(updateSetId) {
var dd = new GlideModal("hierarchical_progress_viewer");
dd.on("beforeclose", function () {
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;
});
dd.setTitle("Progress");
dd.setPreference('sysparm_function', 'publishToUpdateSet');
dd.setPreference('sysparm_update_set_id', updateSetId);
dd.setPreference('sysparm_sys_id', this.appId);
dd.setPreference('sysparm_name', this.inferredUsName);
dd.setPreference('sysparm_version', this.versionField.value);
dd.setPreference('sysparm_description', this.descriptionField.value);
dd.setPreference('sysparm_include_data', this.includeDataField.checked);
dd.setPreference('sysparm_progress_name', "Publishing application");
dd.setPreference('sysparm_ajax_processor', 'com.snc.apps.AppsAjaxProcessor');
dd.setPreference('sysparm_show_done_button', 'true');
dd.render();
GlideDialogWindow.get().destroy();
return dd;
}
Basically, this code replaces the original pop-up with a progress bar driven off of the server side publishToUpdateSet function of the com.snc.apps.AppsAjaxProcessor. This is probably something that we still want to do. We just don’t want to be sent to the Update Set form when it all over. Instead, we want to go ahead and turn it into an XML file, and then we want to attach that file to a new version record for the application. Also, if the application has never been published before, we will need to create the master application record as well. But it looks like we can steal most of this code so far. We will need to clone both the UI Action and the UI Page, and then point our cloned action to our cloned page, which is where we will make the majority of the changes. So far, so good.
Now we need to turn the Update Set into XML. For that, we take a peek at the other UI Action for that purpose, found on the Update Set form. To hunt that guy down, we pull up any local Update Set and use the same hamburger menu options to pull up the UI Actions for that form and look for the one called Export to XML. Here is the relevant script:
var updateSetExport = new UpdateSetExport();
var sysid = updateSetExport.exportUpdateSet(current);
action.setRedirectURL("export_update_set.do?sysparm_sys_id=" + sysid + "&sysparm_delete_when_done=true");
The first couple of lines look important, but that last one looks like it pops up the XML file for downloading, which we definitely do not want to do in our adaptation. It turns out that export_update_set is a Processor, which also has code of its own, so we better dig into that guy and see if does anything important that we might need to retain. We can find that guy by entering the word processors in the left-hand navigation menu and then select the lone menu item that appears to bring up the list. Once we have the list, we just need to search for the one where the Path is export_update_set. Here is the relevant code:
(function process(g_request, g_response, g_processor) {
var sysid = g_request.getParameter('sysparm_sys_id');
var remoteUpdateGR = new GlideRecord('sys_remote_update_set');
if (!remoteUpdateGR.get(sysid)) {
gs.addErrorMessage(gs.getMessage('Update Set is invalid'));
return;
}
if (!gs.hasRole('admin') && !sn_app_api.AppStoreAPI.canPublishToUpdateSet(remoteUpdateGR.application)) {
gs.addErrorMessage(gs.getMessage('You do not have permission to perform this operation'));
g_response.setStatus(403);
return;
}
var exporter = new ExportWithRelatedLists('sys_remote_update_set', sysid);
exporter.addRelatedList('sys_update_xml', 'remote_update_set');
exporter.exportRecords(g_response);
var del = g_request.getParameter('sysparm_delete_when_done');
if (del == "true" && remoteUpdateGR.canDelete())
remoteUpdateGR.deleteRecord();
})(g_request, g_response, g_processor);
So it looks like code in which we are interested is the stuff going on with the variable exporter. Unfortunately, there is no output from the exportRecords function of that object and it takes as input the global g_response object. That will send the output to the browser, which we definitely do not want to do. Maybe we can hack it by sending it a fake g_response object, and then pull our output from there. If you right click on the class name (ExportWithRelatedLists), you can use the resulting context menu to pull up the definition and see exactly what it does. There is quite a bit of code in there, but here is the relevant function:
exportRecords: function(response){
this.setHeaders(response);
var outputStream = response.getOutputStream();
this.hd = this.beginExport(outputStream);
var gr = new GlideRecord(this.parent_table);
gr.get(this.sys_id);
this.exportRecord(gr);
this.exportChildren();
this._exportQuerySets();
this.endExport(outputStream);
}
It basically sends everything to the outputStream that it obtains from the response object that you send it. So, we could build our own response object with our own outputStream, use this component right out of the box, and then grab the outputStream from our phony g_response object. Or, we just steal the code from this guy and use it to build our own exporter and have our version of the exportRecords function simply return the XML. Either way, we should be able to leverage all of this code to do what it is that we want to do. That should give us our XML file, anyway.
Of course, generating the XML from the Scoped Application is just the start of the process. Once we have it in hand, we have to create a version record, attach the XML to the version record as a file, and then ship the thing over to the Host instance. Once it arrives at the Host, we will also have to send the version record and the attached XML file out to all of the other instances. That’s all quite a bit of work, but so was the set-up process, and we just trudged through that effort one piece at a time until we got to the end of the to-do list. Hopefully, we can do the same here. We may jump right into that next time, if we don’t yet have any feedback from the set-up testing.