Collaboration Store, Part XIX

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

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

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

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

	type: 'CollaborationStoreGlobalUtils'
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	type: 'CollaborationStoreGlobalUtils'
};

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