This tutorial applies to Axeda version 6.1.6+, with sections applicable to 6.5+ (indicated below) Custom objects (or Groovy scripts) are the backbone of Axeda custom applications. As the developer, you decide what content type to give the data returned by the script. What this tutorial covers? This tutorial provides examples of outputting data in different formats from Groovy scripts and consuming that data via Javascript using the jQuery framework. While Javascript and jQuery are preferred by the Axeda Innovation team, any front end technology that can consume web services can be used to build applications on the Axeda Machine Cloud. On the same note, the formats discussed in this article are only a few examples of the wide variety of content types that Groovy scripts can output via Scripto. The content types available via Scripto are limited only by their portability over the TCP protocol, a qualification which includes all text-based and downloadable binary mime types. As of July 2013, the UDP protocol (content streaming) is not supported by the current version of the Axeda Platform. Formats discussed in this article: 1) JSON 2) XML 3) CSV 4) Binary content with an emphasis on image files (6.5+) For a tutorial on how to create custom objects that work with custom applications, check out Using Google Charts API with Scripto. For a discussion of what Scripto is and how it relates to Groovy scripts and Axeda web services, take a look at Unleashing the Power of the Axeda Platform via Scripto. Serializing Data JSON For those building custom applications with Javascript, serializing data from scripts into JSON is a great choice, as the data is easily consumable as native Javascript objects. The net.sf.json JSON library is available to use in the SDK. It offers an easy way to serialize objects on the Platform, particularly v2 SDK objects. import net.sf.json.JSONArray import static com.axeda.sdk.v2.dsl.Bridges.* def asset = assetBridge.findById(parameters.assetId) def response = JSONArray.fromObject(asset).toString(2) return ["Content-Type": "application/json", "Content": response] Outputs: [{ "buildVersion": "", "condition": { "detail": "", "id": "3", "label": "", "restUrl": "", "systemId": "3" }, "customer": { "detail": "", "id": "2", "label": "Default Organization", "restUrl": "", "systemId": "2" }, "dateRegistered": { "date": 11, "day": 1, "hours": 18, "minutes": 7, "month": 2, "seconds": 49, "time": 1363025269253, "timezoneOffset": 0, "year": 113 }, "description": "", "detail": "testasset", "details": null, "gateways": [], "id": "12345", "label": "", "location": { "detail": "Default Organization", "id": "2", "label": "Default Location", "restUrl": "", "systemId": "2" }, "model": { "detail": "testmodel", "id": "2345", "label": "standalone", "restUrl": "", "systemId": "2345" }, "name": "testasset", "pingRate": 0, "properties": [ { "detail": "", "id": "1", "label": "TestProperty", "name": "TestProperty", "parentId": "2345", "restUrl": "", "systemId": "1", "value": "" }, { "detail": "", "id": "4", "label": "TestProperty0", "name": "TestProperty0", "parentId": "2345", "restUrl": "", "systemId": "4", "value": "" }, { "detail": "", "id": "3", "label": "TestProperty1", "name": "TestProperty1", "parentId": "2345", "restUrl": "", "systemId": "3", "value": "" }, { "detail": "", "id": "2", "label": "TestProperty2", "name": "TestProperty2", "parentId": "2345", "restUrl": "", "systemId": "2", "value": "" } ], "restUrl": "", "serialNumber": "testasset", "sharedKey": [], "systemId": "12345", "timeZone": "GMT" }] This output can be traversed as Javascript object with its nodes accessible using dot (.) notation. For example, if you set the above JSON as the content of variable "json", you can access it in the following way, without any preliminary parsing needed: assert json[0].condition.id == 3 If you use jQuery, a Javascript library, feel free to make use of axeda.js, which contains utility functions to pass data to and from the Axeda Platform. One function in particular is used in most example custom applications found on this site, the axeda.callScripto function. It relies on the jQuery ajax function to make the underlying call. /** * makes a call to the enterprise platform services with the name of a script and passes * the script any parameters provided. * * default is GET if the method is unknown * * Notes: Added POST semantics - plombardi @ 2011-09-07 * * original author: Zack Klink & Philip Lombardi * added on: 2011/7/23 */ // options - localstoreoff: "yes" for no local storage, contentType: "application/json; charset=utf-8", axeda.callScripto = function (method, scriptName, scriptParams, attempts, callback, options) { var reqUrl = axeda.host + SERVICES_PATH + 'Scripto/execute/' + scriptName + '?sessionid=' + SESSION_ID var contentType = options.contentType ? options.contentType : "application/json; charset=utf-8" var local var daystring = keygen() if (options.localstoreoff == null) { if (localStorage) { local = localStorage.getItem(scriptName + JSON.stringify(scriptParams)) } if (local != null && local == daystring) { return dfdgen(reqUrl + JSON.stringify(scriptParams)) } else { localStorage.setItem(scriptName + JSON.stringify(scriptParams), daystring) } } return $.ajax({ type: method, url: reqUrl, data: scriptParams, contentType: contentType, dataType: "text", error: function () { if (attempts) { expiredSessionLogin(); setTimeout(function () { axeda.callScripto('POST', scriptName, scriptParams, attempts - 1, callback, options) }, 1500); } }, success: function (data) { if (options.localstoreoff == null) { localStorage.setItem(reqUrl + JSON.stringify(scriptParams), JSON.stringify([data])) } if (contentType.match("json")) { callback(unwrapResponse(data)) } else { callback(data) } } }) }; Using the axeda.callScripto function: var postToPlatform = function (scriptname, callback, map) { var options = { localstoreoff: "yes", contentType: "application/json; charset=utf-8" } // Javascript object "map" has to be stringified to post to Axeda Platform axeda.callScripto("POST", scriptname, JSON.stringify(map), 2, function (json) { // callback gets the JSON object output by the Groovy script callback(json) }, options) } The JSON object is discussed in more detail here. Back to Top XML XML is the preferred language of integration with external applications and services. Groovy provides utilities to make XML serialization a trivial exercise. import groovy.xml.MarkupBuilder import static com.axeda.sdk.v2.dsl.Bridges.* def writer = new StringWriter() def xml = new MarkupBuilder(writer) def findAssetResult = assetBridge.find(new AssetCriteria(modelNumber: parameters.modelName)) // find operation returns AssetReference class. Contains asset id only def assets = findAssetResult.assets xml.Response() { Assets() { assets.each { AssetReference assetRef -> def asset = assetBridge.findById(assetRef.id) // asset contains a ModelReference object instead of a Model. ModelReference has a detail property, not a name property Asset() { id(asset.id) name(asset.name) serial_number(asset.serialNumber) model_id(asset.model.id) model_name(asset.model.detail) } } } } return ['Content-Type': 'text/xml', 'Content': writer.toString()] Output: <Assets> <Asset> <id>98765</id> <name>testasset</name> <serial_number>testasset</serial_number> <model_id>4321</model_id> <model_name>testmodel</model_name> </Asset> </Assets Although XML is not a native Javascript object as is JSON, Javascript libraries and utilities are available for parsing XML into an object traversable in Javascript. For more information on parsing XML in Javascript, see W3 Schools XML Parser. For those using jQuery, check out the jQuery.parseXML function. Back to Top Outputting Files (Binary content types) CSV CSV comes in handy for spreadsheet generation as it is compatible with Microsoft Excel. The following example is suitable for Axeda version 6.1.6+ as it makes use of the Data Accumulator feature to create a downloadable file. import com.axeda.drm.sdk.device.ModelFinder import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.scripto.Request import com.axeda.common.sdk.id.Identifier import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.DataItem import com.axeda.drm.sdk.device.DataItemValue import com.axeda.drm.sdk.data.DataValue import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.device.Device import com.axeda.drm.sdk.mobilelocation.MobileLocation import com.axeda.drm.sdk.data.DataValueList import com.axeda.drm.sdk.data.CurrentDataFinder import com.axeda.drm.sdk.mobilelocation.CurrentMobileLocationFinder import groovy.xml.MarkupBuilder import com.axeda.platform.sdk.v1.services.ServiceFactory /* * ExportObjectToCSV.groovy * * Creates a csv file from either all assets of a model of a single asset that can then be used to import them back into another system. * * @param model - (REQ):Str model name. * @param serial - (OPT):Str serial number. * * @author Sara Streeter <sstreeter@axeda.com> */ def writer = new StringWriter() def xml = new MarkupBuilder(writer) InputStream is try { Context CONTEXT = Context.getSDKContext() ModelFinder modelFinder = new ModelFinder(CONTEXT) modelFinder.setName(Request.parameters.model) Model model = modelFinder.find() DeviceFinder deviceFinder = new DeviceFinder(CONTEXT) deviceFinder.setModel(model) List<Device> devices = [] def exportkey = model.name Device founddevice if (Request.parameters.serial){ deviceFinder.setSerialNumber(Request.parameters.serial) founddevice = deviceFinder.find() logger.info(founddevice?.serialNumber) if (founddevice != null){ devices.add(founddevice) } else throw new Exception("Device ${Request.parameters.serial} cannot be found.") exportkey += "${founddevice.serialNumber}" } else { devices = deviceFinder.findAll() exportkey += "all" } // use a Data Accumulator to store the information def dataStoreIdentifier = "FILE-CSV-export_____" + exportkey def daSvc = new ServiceFactory().dataAccumulatorService if (daSvc.doesAccumulationExist(dataStoreIdentifier, devices[0].id.value)) { daSvc.deleteAccumulation(dataStoreIdentifier, devices[0].id.value) } List<DataItem> dataItemList = devices[0].model.dataItems def firstrow = [ "model", "serial", "devicename", "conditionname", "currentlat","currentlng" ] def tempfirstrow = dataItemList.inject([]){list, dataItem -> list << dataItem.name; list } firstrow += tempfirstrow firstrow = firstrow.join(',') firstrow += '\n' daSvc.writeChunk(dataStoreIdentifier, devices[0].id.value, firstrow); CurrentMobileLocationFinder currentMobileLocationFinder = new CurrentMobileLocationFinder(CONTEXT) devices.each{ device -> CurrentDataFinder currentDataFinder = new CurrentDataFinder(CONTEXT, device) currentMobileLocationFinder.deviceId = device.id.value MobileLocation mobileLocation = currentMobileLocationFinder.find() def lat = 0 def lng = 0 if (mobileLocation){ lat = mobileLocation?.lat lng = mobileLocation?.lng } def row = [ device.model.name, device.serialNumber, device.name, device.condition?.name, lat, lng ] def temprow = dataItemList.inject([]){ subList,dataItem -> DataValue value = currentDataFinder.find(dataItem.name) def val = "NULL" val = value?.asString() != "?" ? value?.asString() : val subList << val subList } row += temprow row = row.join(',') row += '\n' daSvc.writeChunk(dataStoreIdentifier, devices[0].id.value, row); } // stream the data accumulator to create the file is = daSvc.streamAccumulation(dataStoreIdentifier, devices[0].id.value) def disposition = 'attachment; filename=CSVFile' + exportkey + '.csv' return ['Content-Type': 'text/csv', 'Content-Disposition':disposition, 'Content': is.text] } catch (def ex) { xml.Response() { Fault { Code('Groovy Exception') Message(ex.getMessage()) StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); Detail(sw.toString()) } } logger.info(writer.toString()) return ['Content-Type': 'text/xml', 'Content': writer.toString()] } return ['Content-Type': 'text/xml', 'Content': writer.toString()] Back to Top Image Files (6.5+) The FileStore in Axeda version 6.5+ allows fine-grained control of uploaded and downloaded files. As Groovy scripts can return binary data via Scripto, this allows use cases such as embedding a Groovy script url as the source for an image. The following example uses the FileStore API to create an Image out of a valid image file, scales it to a smaller size and stores this smaller file. import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.data.* import com.axeda.drm.sdk.device.* import com.axeda.drm.sdk.mobilelocation.CurrentMobileLocationFinder import com.axeda.drm.sdk.mobilelocation.MobileLocation import com.axeda.drm.sdk.mobilelocation.MobileLocationFinder import com.axeda.sdk.v2.bridge.FileInfoBridge import static com.axeda.sdk.v2.dsl.Bridges.* import com.axeda.services.v2.ExecutionResult import com.axeda.services.v2.FileInfo import com.axeda.services.v2.FileInfoReference import com.axeda.services.v2.FileUploadSession import net.sf.json.JSONObject import groovy.json.JsonBuilder import net.sf.json.JSONArray import com.axeda.drm.sdk.scripto.Request import org.apache.commons.io.IOUtils import org.apache.commons.lang.exception.ExceptionUtils import com.axeda.common.sdk.id.Identifier import groovy.json.* import javax.imageio.ImageIO; import java.awt.RenderingHints import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream; import java.awt.* import java.awt.geom.* import javax.imageio.* import java.awt.image.* import java.awt.Graphics2D import javax.imageio.stream.ImageInputStream /* Image-specific FileStore entry point to post and store files */ def contentType = "application/json" final def serviceName = "StoreScaledImage" // Create a JSON Builder def json = new JsonBuilder() // Global try/catch. Gotta have it, you never know when your code will be exceptional! try { Context CONTEXT = Context.getSDKContext() def filesList = [] def datestring = new Date().time InputStream inputStream = Request.inputStream def reqbody = Request.body // all of our Request Parameters are available here def params = Request.parameters def filename = Request?.headers?.'Content-Disposition' ? Request?.headers?.'Content-Disposition' : "file___" + datestring + ".txt" def filelabel = Request.parameters.filelabel ?: filename def description = Request.parameters.description ?: filename def contType = Request.headers?."content-type" ?: "image/jpeg" def tag = Request.parameters.tag ?: "cappimg" def encoded = Request.parameters.encoded?.toBoolean() def dimlimit = params.dimlimit ? params.dimlimit : 280 // host is available in the headers when the script is called with AJAX def domain = Request.headers?.host byte[] bytes = IOUtils.toByteArray(inputStream); def fileext = filename.substring(filename.indexOf(".") + 1,filename.size()) def outerMap = [:] // check that file extension matches an image type if (fileext ==~ /([^\s]+(\.(?i)(jpg|jpeg|png|gif|bmp))$)/){ if (inputStream.available() > 0) { def scaledImg try { def img = ImageIO.read(inputStream) def width = img?.width def height = img?.height def ratio = 1.0 def newBytes if (img){ if (width > dimlimit || height > dimlimit){ // shrink by the smaller side so it can still be over the limit def dimtochange = width > height ? height : width ratio = dimlimit / dimtochange width = Math.floor(width * ratio).toInteger() height = Math.floor(height * ratio).toInteger() } newBytes = doScale(img, width, height, ratio, fileext) if (newBytes?.size() > 0){ bytes = newBytes } } } catch(Exception e){ logger.info(e.localizedMessage) } outerMap.byteCount = bytes.size() FileInfoBridge fib = fileInfoBridge FileInfo myImageFile = new FileInfo(filelabel: filelabel, filename: filename, filesize: bytes?.size(), description: description, tags: tag ) myImageFile.contentType = contType FileUploadSession fus = new FileUploadSession(); fus.files = [myImageFile] ExecutionResult fer = fileUploadSessionBridge.create(fus); myImageFile.sessionId = fer.succeeded.getAt(0)?.id ExecutionResult fileInfoResult = fib.create(myImageFile) if (fileInfoResult.successful) { outerMap.fileInfoSave = "File Info Saved" outerMap.sessionId = "File Upload SessionID: "+fer.succeeded.getAt(0)?.id outerMap.fileInfoId = "FileInfo ID: "+fileInfoResult?.succeeded.getAt(0)?.id ExecutionResult er = fib.saveOrUpdate(fileInfoResult.succeeded.getAt(0).id,new ByteArrayInputStream(bytes)) def fileInfoId = fileInfoResult?.succeeded.getAt(0)?.id String url = "${domain}/services/v1/rest/Scripto/execute/DownloadFile?fileId=${fileInfoId}" if (er.successful) { outerMap.url = url } else { outerMap.save = "false" logger.info(logFailure(er,outerMap)) } } else { logger.info(logFailure(fileInfoResult, outerMap)) } } else { outerMap.bytesAvail = "No bytes found to upload" } } else { outerMap.imagetype = "Extension $fileext is not a supported image file type." } filesList << outerMap // return the JSONBuilder contents // we specify the content type, and any object as the return (even an outputstream!) return ["Content-Type": contentType,"Content":JSONArray.fromObject(filesList).toString(2)] // alternately you may just want to serial an Object as JSON: // return ["Content-Type": contentType,"Content":JSONArray.fromObject(invertedMessages).toString(2)] } catch (Exception e) { // I knew you were exceptional! // we'll capture the output of the stack trace and return it in JSON json.Exception( description: "Execution Failed!!! An Exception was caught...", stack: ExceptionUtils.getFullStackTrace(e) ) // return the output return ["Content-Type": contentType, "Content": json.toPrettyString()] } def doScale(image, width, height, ratio, fileext){ if (image){ ByteArrayOutputStream baos = new ByteArrayOutputStream(); def bytes def scaledImg = new BufferedImage( width, height, BufferedImage.TYPE_INT_RGB ) Graphics2D g = scaledImg.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.scale(ratio,ratio) g.drawImage(image, null, null); g.dispose(); ImageIO.write( scaledImg, fileext, baos ) baos.flush() bytes = baos.toByteArray() baos.close() } else { logger.info("image to be scaled is null") return false } return bytes } private void logFailure(ExecutionResult fileInfoResult, LinkedHashMap outerMap) { outerMap.message = fileInfoResult.failures.getAt(0)?.message outerMap.source = fileInfoResult.failures.getAt(0)?.sourceOfFailure outerMap.details = fileInfoResult.failures.getAt(0)?.details?.toString() outerMap.fileInfoSave = "false" } The next example makes use of the jQuery framework to upload an image to this script via an http POST. Note: This snippet is available as a jsFiddle at http://jsfiddle.net/LrxWF/18/ With HTML5 button: <input type="file" id="fileinput" value="Upload" /> var PLATFORM_HOST = document.URL.split('/apps/')[0]; // this is how you would retrieve the host on an Axeda instance var SESSION_ID = null // usually retrieved from login function included below /*** * Depends on jQuery 1.7+ and HTML5, assumes an HTML5 element such as the following: * <input type="file" id="fileinput" value="Upload" /> * **/ $("#fileinput").off("click.filein").on("click.filein", function () { fileUpload() }) var fileUpload = function () { $("#fileinput").off('change.fileinput') $("#fileinput").on('change.fileinput', function (event) { if (this.files && this.files.length > 0) { handleFiles("http://" + PLATFORM_HOST, this.files) } }) } var handleFiles = function (host, files) { $.each(files, function (index, file) { var formData = new FormData(); var filename = file.name formData.append(filename, file) var url = host + '/services/v1/rest/Scripto/execute/StoreScaledImage?filelabel=' + filename + "&tag=myimg" url = setSessionId(url) jQuery.ajax(url, { beforeSend: function (xhr) { xhr.setRequestHeader('Content-Disposition', filename); }, cache: false, cache: false, processData: false, type: 'POST', contentType: false, data: formData, success: function (json) { refreshPage(json) console.log(json) } }); }) } var setSessionId = function (url) { // you would already have this from logging in return url + "&sessionid=" + SESSION_ID } var refreshPage = function (json) { // here you would refresh your page with the returned JSON return } /*** * The following functions are not used in this demonstration, however they are necessary for a complete app and are found in axeda.js http://gist.github.com/axeda/4340789 ***/ function login(username, password, success, failure) { var reqUrl = host + SERVICES_PATH + 'Auth/login'; localStorage.clear() return $.get(reqUrl, { 'principal.username': username, 'password': password }, function (xml) { var sessionId = $(xml).find("ns1\\:sessionId, sessionId").text() // var sessionId = $(xml).find("[nodeName='ns1:sessionId']").text(); - no longer works past jquery 1.7 if (sessionId) { // set the username and password vars for future logins. storeSession(sessionId); success(SESSION_ID); // return the freshly stored contents of SESSION_ID } else { failure($(xml).find("faultstring").text()); } }).error(function () { $('#loginerror').html('Login Failed, please try again') }); }; function storeSession(sessionId) { var date = new Date(); date.setTime(date.getTime() + SESSION_EXPIRATION); SESSION_ID = sessionId document.cookie = APP_NAME + '_sessionId=' + SESSION_ID + '; expires=' + date.toGMTString() + '; path=/'; return true; }; The return JSON includes a URL that you can use as the source for images: [{ "byteCount": 14863, "fileInfoSave": "File Info Saved", "sessionId": "File Upload SessionID: 01234", "fileInfoId": "FileInfo ID: 12345", "url": "http://yourdomain.axeda.com/services/v1/rest/Scripto/execute/DownloadFile?fileId=12345" }] The DownloadFile Custom Object looks like the following: import static com.axeda.sdk.v2.dsl.Bridges.* import javax.activation.MimetypesFileTypeMap import com.axeda.services.v2.* import com.axeda.sdk.v2.exception.* import com.axeda.drm.sdk.scripto.Request def knowntypes = [ [png: 'image/png'] ,[gif: 'image/gif'] ,[jpg: 'image/jpeg'] ] def params = Request.parameters.size() > 0 ? Request.parameters : parameters def response = fileInfoBridge.getFileData(params.fileId) def fileinfo = fileInfoBridge.findById(params.fileId) def type = fileinfo.filename.substring(fileinfo.filename.indexOf('.') + 1,fileinfo.filename.size()) type = returnType(knowntypes, type) def contentType = params.type ?: (type ?: 'image/jpg') return ['Content': response, 'Content-Disposition': contentType, 'Content-Type':contentType] def returnType(knowntypes, ext){ return knowntypes.find{ it.containsKey(ext) }?."$ext" } Make sure to append a valid session id to the end of the URL when using it as the source for an image. The techniques discussed above can be applied to any type of binary file output with consideration for the type of file being processed. A Word on Streaming Content streaming such as streaming of video or audio files over UDP is not currently supported by the Axeda Platform.
View full tip