My previous posting outlined the thinking behind openMDWS. In this article I’d like to delve into the technical stuff and show, by use of a few examples, how openMDWS services can be created. If you’re not a technical person, this posting may not be for you!
The first and key step is to be clear about what the openMDWS service you’re going to define will do. It can be read-only or read-write, but whatever it is and whatever it does, it must be designed to work in isolation – a completely atomic function whose operation depends solely on the inputs with which it’s provided. You’ll need to think of a sensible name for the openMDWS service that describes its functionality: this name should be a camel-cased name, usually prefixed by get, set or create, eg getVitals, setProblem, createAppointment.
The examples I’m going to use are login (the process by which you are authenticated on a VistA server, via your access code and verify code) and getClinics (which will return a list of the clinics that are defined for the VistA server you’ve logged into).
The next step is to create the core openMDWS function. This is a Mumps function that can reside in any Caché routine or GT.M routine file (I’ll explain how openMDWS functions are registered and discovered later). All core openMDWS functions have the same signature:
– two mandatory arguments, each of which is an array: one for inputs, the other for the results generated by the function. The inputs array should be a list of name/value pairs. The results array can use any depth of subscripting and its structure should be appropriate to the nature and complexity of the data generated by the function;
– a single return value which is a null string if the function ran to completion without error, or a text string which contains a description of the error that occurred. If an error occurs, additional information about the error can be conveyed in the results array.
The core openMDWS function otherwise contains legacy VistA code, but:
– all local variables required by that VistA code must come from the inputs array: it cannot leak in via globally-scoped local variables
– any local variables generated by the VistA code must be scoped to exist only within the function: ie nothing should leak out of the function.
On a stylistic note, I would like to see openMDWS core functions adopt a more modern coding style to make them more readable, maintainable and palatable to the current, new generation of developers. My suggested stylistic standards include:
– all local variables to be declared within one or more new commands at the top of the function (ie not scattered around within the function), and the variable list to be declared in alphabetic order (for ease of identification and maintenance);
– all names of local variables and local arrays that are not internal to VistA itself to by camel-cased and be meaningful names, up to 31 characters in length. The old standard within VistA of limiting names to 8 upper-case characters is no longer needed, since all the major commercial vendors of Mumps that support VistA have long since broken free of this serious limitation to code readability and maintainability;
– all Mumps commands and functions to be in lower case. Personally I tend to reduce commands to a single letter, but I am happy for developers to use the full command name. Upper cased Mumps coding looks old fashioned and designers will confirm that lower-case text is easier to read and understand than text that is all in upper case.
– the one exception to the above is that a Quit command that terminates and exits the function should be spelt out in full and in upper case. This allows the reader to quickly and easily identify all the exit points from the function.
– when using inline functions that use the Mumps “dot” notation, to add a space between each dot where inline functions are nested;
– any local variables that are used exclusively within an inline function should be declared using a new command at the start of the inline function.
With these in mind, here’s what the getClinics core function might look like:
getClinics(inputs,results) ; n clinics,DT,no,U ; s U="^" s DT=$g(inputs("DT")) k results d CLINLOC^ORWU(.clinics,$g(inputs("seed")),1) s no="" f s no=$o(clinics(no)) q:no="" d . s results("clinics",no,"ien")=$p(clinics(no),U,1) . s results("clinics",no,"name")=$p(clinics(no),U,2) . s results("count")=no ; QUIT ""
You’ll notice that this function requires the standard VistA local variable DT to exist in order to run, so DT is instantiated within the function and its value is conveyed into the function via the local array member inputs(“DT”). This function has a single exit point and will always run without signalling any error: worse case scenario is that no clinics are found so the results array is empty.
The login function is more complex, not least because it must check and validate the user’s access code and verify code, and return a variety of errors depending on the values that are supplied to the function. The other interesting feature is the sheer number of local variables that have to be declared in order to limit their scope: this indicates the frightening leakiness of the legacy VistA code that is required for the login process and that VistA developers have usually had to live with! Here’s the login function:
login(inputs,results) ; n %,accessCode,accver,DILOCKTM,displayPersonName,DISYS,%DT,DT,DTIME,DUZ,%H n %I,I,IO,IOF,IOM,ION,IOS,IOSL,IOST,IOT,J,personDuz,personName n POP,termReason,U,user,V4WVCC,V4WCVMSG n X,XOPT,XPARSYS,XQVOL,XQXFLG,XUCI,XUDEV,XUENV,XUEOFF,XUEON n XUF,XUFAC,XUIOP,XUVOL,XWBSTATE,XWBTIME,Y,verifyCode ; s accessCode=$g(inputs("accessCode")) i accessCode="" s accessCode=$g(inputs("username")) i accessCode="" QUIT "Missing account ID" ; s verifyCode=$g(inputs("verifyCode")) i verifyCode="" s verifyCode=$g(inputs("pwd")) i verifyCode="" QUIT "Missing account password" ; k results s U="^" d NOW^%DTC s DT=X s (IO,IO(0),IOF,IOM,ION,IOS,IOSL,IOST,IOT)="" s POP=0 ; s accver=accessCode_";"_verifyCode s accver=$$ENCRYP^XUSRB1(accver) d SETUP^XUSRB() d VALIDAV^XUSRB(.user,accver) s personDuz=user(0) ; s V4WVCC=$g(user(2)) s V4WCVMSG=$g(user(3)) ;sign in message ; s termReason="" i 'personDuz,$G(DUZ) s termReason=": "_$$GET1^DIQ(200,DUZ_",",9.4) ;Termination reason i 'personDuz QUIT user(3)_termReason ; s personName=$p(^VA(200,personDuz,0),"^") s displayPersonName=$p(personName,",",2)_" "_$p(personName,",") ; s results("DT")=DT s results("DUZ")=personDuz s results("username")=personName s results("displayName")=displayPersonName s results("greeting")=$g(user(7)) QUIT ""
You’ll notice that the login function’s results array returns a number of key VistA-specific values that are needed by the VistA legacy code behind other openMDWS functions: we’ve already seen how the getClinics function needed DT, and you can now see that this originally comes from successful invocation of the login function.
So we now have two openMDWS core functions. You can see that these can be tested and run in isolation. A test I perform when writing such functions is to ensure that all the variables within the function are properly scoped and not leaking out. For example on a Caché system I’ll do the following kind of thing within a terminal (shell) session:
kill ; clear down the symbol table set inputs("accessCode")="xxxxx" set inputs("verifyCode")="yyyyy" set ok=$$login^myMDWSCoreFuncs(.inputs,.results) ; now test to see if anything other than ok, inputs and results are in the ; symbol table write
If any other variables are listed, then add them to the new list at the top of the core function. Repeat this exercise until the function runs cleanly without any variable leakage.
We now have two completely atomic, self-standing functions that wrapper core VistA code. That old VistA code has been encapsulated once and for all, and now never needs to be seen by an application developer: he/she just needs to invoke the core openMDWS function.
The next step is to create a web service wrapper around each of these functions. The heavy lifting is all carried out for you by EWD, so you just need to focus on mapping the contents of the core function’s results array to another local array that reflects the structure of the XML response that you want your web service to create.
The key part of this stage is whether you’re writing an openMDWS service that emulates an existing VA MDWS operation, or whether you’re creating your own service (ie extending what’s available in MDWS). If you’re emulating an existing MDWS operation, then you’ll need to know the structure of the XML that the pukka MDWS operation creates. There’s a number of ways you can do this, but, for example, here’s an example of the login operation’s XML response:
<UserTO xmlns="http://mdws.medora.va.gov/EmrSvc" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <name>ADMINISTRATOR,SYSTEM</name> <SSN /> <DUZ>1</DUZ> <siteId>100</siteId> <greeting>Good morning SA</greeting> </UserTO>
The current release of openMDWS emulates the MDWS HTTP GET operations (SOAP versions will be developed if there is demand). So the above example is the result of the MDWS HTTP GET invocation of the login operation.
The trick with openMDWS is that you don’t need to build that XML directly: you simply need to create a local array whose subscripting reflects the tag hierarchy of the XML document you’re wanting to create. EWD will automatically map that array into the corresponding XML and dispatch it as an HTTP response. So here’s the openMDWS web service wrapper around the login core function:
login(sessid,localCall) ; n array,attrs,facade,inputs,ok,originalSessid,outerTag,results,siteId ; s ok=$$invoke^%zewdMDWS("login",localCall,.results,.originalSessid,sessid) i 'ok QUIT "" ; s siteId=$$getSessionValue^%zewdAPI("vista.systemId",originalSessid) s outerTag="UserTO" d outerTag^%zewdMDWS(.array,outerTag,sessid) s array(outerTag,"name")=$g(results("username")) s array(outerTag,"SSN")=$g(results("SSN")) s array(outerTag,"DUZ")=$g(results("DUZ")) s array(outerTag,"siteId")=siteId s array(outerTag,"greeting")=$g(results("greeting")) d createOutput^%zewdMDWS(localCall,.array,sessid) ; QUIT ""
There are a number of things to point out here:
– the signature of all openMDWS web service wrappers is the same: two arguments (sessid and localCall) that are handled automatically by EWD so don’t worry about where they come from;
– the function should always terminate with a null string as its returnValue;
– you invoke the core openMDWS function using the call $$invoke^%zewdMDWS. Its first argument indicates the name of the openMDWS core function you wish to invoke. We’ll look later at how this name is registered within openMDWS;
– the remaining arguments of the invoke() function are mandatory and should always be expressed as in the example above;
– you should always test the value returned by the invoke() function: if it’s a zero (false) value, it means that the invocation failed for some reason and a MDWS-compatible XML Fault response will have been generated;
– if the core function worked OK, then you map the results array to the XML-mapped array. Use the convenience function outerTag^%zewdMDWS to create the outer tag (in this case “UserTo“). This function ensures that the correct namespace declarations are added as attributes to the outer tag;
– after that, just create the array nodes that map to the XML you’re generating. For example, to create the XML tag <name> inside the <UserTO> tag:
– Finally you get EWD to map this array to the XML document using the call to createOutput^%zewdMDWS. Once again, always use the arguments as shown in the example above.
That’s it: we now have an openMDWS version of the login MDWS operation.
There’s one last step: registering your openMDWS method and its corresponding functions. openMDWS provides two techniques:
– the EWD routine %zewdMDWSMap (_zewdMDWSMap.m on GT.M systems) contains a list of JSON-based definitions for openMDWS methods known to me at the time the particular EWD build was created;
– you can define your own mappings in the global ^zewd(“openMDWS”):
^zewd("openMDWS","userDefined",facadeName,operationName,"coreMethod") = functionReference ^zewd("openMDWS","userDefined",facadeName,operationName,"MDWSWrapper") = functionReference where: facadeName = the name of the MDWS/openMDWS facade (facades provide a means of sub-grouping services) operationName = the name of the openMDWS method/operation functionReference = the reference to the relevant Mumps function, eg: ^zewd("openMDWS","userDefined","EmrSvc","match","coreMethod") = "match^%zewdVistARPC" ^zewd("openMDWS","userDefined","EmrSvc","match","MDWSWrapper") = "match^%zewdMDWSSvc1"
You then need to add this to the registered list:
That’s it as far as defining openMDWS methods and their web service wrappers. In the next posting I’ll explain how to use openMDWS web services from within EWD applications.
Note: openMDWS is only available if you install EWD build 942 or later. I’ll be releasing this very soon for those of you who want to get started developing using openMDWS!