Less Common Tasks
This chapter discusses the following less common development tasks:
Defining Custom Utility Functions
Ensemble provides a set of utility functions that can be invoked from business rules and from DTL; these are described in “Ensemble Utility Functions” in Developing Business Rules. You can add your own functions, and the business rules engine and Business Rule Editor accommodate your extensions automatically.
To add a new utility function:
-
Create a new class that is a subclass of Ens.Rule.FunctionSetOpens in a new tab. This class must not extend any other superclasses, only Ens.Rule.FunctionSetOpens in a new tab.
-
For each function you wish to define, add a class method to your new function set class. There is no support for polymorphism, so to be precise, you must mark these class methods as final. You can view this in the existing Ens.Util.FunctionSetOpens in a new tab methods (Ens.Util.FunctionSetOpens in a new tab is a superclass of Ens.Rule.FunctionSetOpens in a new tab).
-
Compile the new class. The new functions are now available for use in rule expressions. To invoke these functions, use the ClassMethod name from your subclass. Unlike functions defined in Ens.Rule.FunctionSetOpens in a new tab, user-defined method names must be fully qualified with the class that they belong to. This happens automatically if you add them by selecting names from the wizards in the Management Portal.
As an example, the following function set class provides date and time functions for use in business rules. Its class methods DayOfWeek() and TimeInSeconds() invoke the ObjectScript functions $ZDATE and $PIECE to extract the desired date and time values from the ObjectScript special variable $HOROLOG:
/// Time functions to use in rule definitions.
Class Demo.HL7.MsgRouter.Functions Extends Ens.Rule.FunctionSet
{
/// Returns the ordinal position of the day in the week,
/// where 1 is Sunday, 2 is Monday, and so on.
ClassMethod DayOfWeek() As %Integer [ CodeMode = expression, Final ]
{
$zd($H,10)
}
/// Returns the time as a number of seconds since midnight.
ClassMethod TimeInSeconds() As %Integer [ CodeMode = expression, Final ]
{
$p($H,",",2)
}
}
The default language inside a method is ObjectScript, as in the previous example, but you can also use Caché Basic if you define the method using the Language keyword as described in:
-
The “Language” entry in the “Class Keywords” section of the Caché ObjectScript Reference.
-
The “Methods” chapter in Using Caché Objects.
For a full list of functions and special variables available in Caché Basic or ObjectScript, see:
Once you have added a new function as described in this topic, the syntax for referring to it is slightly different than for built-in functions. Suppose you define this function in a class that inherits from Ens.Rule.FunctionSetOpens in a new tab.
ClassMethod normalizaSexo(value as %String) as %String
After you compile the class, when you use one of the Ensemble visual tools such as the Routing Rule Editor or Data Transformation Builder, you see your function name normalizaSexo included in the function selection box along with built-in functions like Strip, In, Contains, and so on.
Suppose you choose a built-in function from the function selection box and look at the generated code. You see that Ensemble has generated the function call using double-dot method call syntax (in DTL) or has simply referenced the function by name (in a business rule). (These syntax rules are explained in “Ensemble Utility Functions” in Developing Business Rules.)
The following example is an <assign> statement from DTL that references the built-in Strip function with double-dot syntax:
<assign property='target.cod'
value='..Strip(source.{PatientIDExternalID.ID},"<>CW")'
action='set'/>
However, if you create your own, user-defined functions, the syntax for DTL is different. It is not enough simply to identify the function; you must also identify the full class name for the class that contains the class method for your function. Suppose your function normalizaSexo was defined in a class named HP.Util.funciones. In that case, after you chose a function from the function selection box and looked at the generated code, you would see something like the following example:
<assign property='target.sexo'
value='##class(HP.Util.funciones).normalizaSexo(source.{Sex})'
action='set'/>
You need to be aware of this syntax variation if you wish to type statements like this directly into your DTL code, rather than using the Data Transformation Builder to generate the code.
Rendering Connections When the Targets Are Dynamic
The Management Portal automatically displays the connections to and from a given business host, when a user selects that business host. For example:
![generated description: production connections](images/egdv_production_connections.png)
To do this, Ensemble reads the configuration settings for the business host and uses them.
If, however, the business service host its targets dynamically, at runtime, Ensemble cannot automatically display such connections. In this case, to display such connections, implement the OnGetConnections() callback method. Ensemble automatically calls this method (which does nothing by default) when it renders the configuration diagram.
OnGetConnections() has the following signature:
ClassMethod OnGetConnections(Output pArray As %String, item As Ens.Config.Item) [ CodeMode = generator ]
Where the arguments are as follows:
-
pArray — A string that this method creates. When the method returns, this string must contain a comma-separated list of the configured names of items to which this business service sends messages. Alternatively, if there are no targets at the present time, this string may be blank ("").
-
item — The Ens.Config.ItemOpens in a new tab object that represents this business service.
For examples of overridden OnGetConnections() methods, use Studio to examine the built-in business services provided for use with electronic data interchange protocols such as HL7 and X12. These are described in detail in books such as the Ensemble HL7 Version 2 Development Guide and the Ensemble X12 Development Guide.
Using Ens.Director to Start and Stop a Production
During development, you typically use the Management Portal to start and stop a production. For live, deployed production, InterSystems recommends that you use the auto-start option as described in Configuring Ensemble Productions.
Another option is to start or stop a production programmatically. To do so, invoke the following methods in the Ens.DirectorOpens in a new tab class:
Stop the currently running production in an Ensemble namespace:
Do ##class(Ens.Director).StopProduction()
Start the specified production in the Ensemble namespace, as long as no other production is running:
Do ##class(Ens.Director).StartProduction("myProduction")
Clean up a Troubled instance of a running production so that you can run a new instance in the Ensemble namespace:
Do ##class(Ens.Director).RecoverProduction()
It is not necessary to call GetProductionStatus() to see if the production terminated abnormally prior to calling RecoverProduction(). If the production is not Troubled, the method simply returns.
This method returns the production status via two output parameters, both of which are passed by reference. The first parameter returns the production name, but only when the status is Running, Suspended, or Troubled. The second parameter returns the production state, which is a numeric value equivalent to one of the following constants:
-
$$$eProductionStateRunning
-
$$$eProductionStateStopped
-
$$$eProductionStateSuspended
-
$$$eProductionStateTroubled
For example:
Set tSC=##class(Ens.Director).GetProductionStatus(.tProductionName,.tState)
Quit:$$$ISERR(tSC)
If tState'=$$$eProductionStateRunning {
$$$LOGINFO($$$Text("No Production is running.")) Quit
}
You can use the production state macros such as $$$eProductionStateRunning in code outside of the Ensemble classes, for example in a general class or routine. To do this, you must add the following statement to the class:
#include Ensemble
It is not necessary to do this inside Ensemble classes, such as in business hosts.
Ens.DirectorOpens in a new tab provides many class methods, including many intended for use only by the Ensemble internal framework. InterSystems recommends that you use only the Ens.DirectorOpens in a new tab methods documented in this book, and only as documented.
InterSystems recommends you do not use the ^%ZSTART routine to control Ensemble production startup. The Ensemble startup mechanisms are much easier to use and are more closely tied to the production itself.
Using Ens.Director to Access Settings
The following Ens.DirectorOpens in a new tab class methods allow retrieval of production settings even when the production is not running:
Returns an array containing the values of all adapter settings for the identified configuration item: a business service or business operation. The array is subscripted by setting name. You can use the Caché $ORDER function to access the elements of the array. The first parameter for this method is a string that contains the production name and configuration item name separated by two vertical bars (||). The return value is a status value. If the status value is not $$$OK, the specified combination of production name (myProd) and configuration item name (myOp) could not be found.
Set tSC=##class(Ens.Director).GetAdapterSettings("myProd||myOp",.tSettings)
Returns the value of a named adapter setting for the identified configuration item: a business service or business operation. The first parameter is a string that contains the production name and configuration item name separated by two vertical bars (||). The second parameter is the name of a configuration setting. The third output parameter returns a status value from the call. For example:
Set val=##class(Ens.Director).GetAdapterSettingValue("myProd||myOp","QSize",.tSC)
If the returned status value is not $$$OK, the specified combination of production name (myProd) and configuration item name (myOp) could not be found, or a setting of the specified name (QSize) was not found in the configuration for that specified production and configuration item.
Returns an array containing the values of all production settings from the currently running production or the production most recently run. The array is subscripted by setting name. The return value for this method is a status value. If the status value is not $$$OK, no current production could be identified.
Set tSC=##class(Ens.Director).GetCurrProductionSettings(.tSettings)
Returns the string value of a named production setting from the currently running production or the production most recently run. The second output parameter returns a status value from the call. If this status value is not $$$OK, either a setting of the specified name was not found in the configuration for the current production, or no current production could be identified.
Set myValue=##class(Ens.Director).GetCurrProductionSettingValue("mySet",.tSC)
Returns an array containing the values of all settings for the identified configuration item: a business service, business process, or business operation. The array is subscripted by setting name. The first parameter for this method is a string that contains the production name and configuration item name separated by two vertical bars (||). The return value is a status value. If the status value is not $$$OK, the specified combination of production name (myProd) and configuration item name (myOp) could not be found.
Set tSC=##class(Ens.Director).GetHostSettings("myProd||myOp",.tSettings)
Returns the value of a named setting for the identified configuration item: a business service, business process, or business operation. The first parameter is a string that contains the production name and configuration item name separated by two vertical bars (||). The second parameter is the name of a configuration setting. The third output parameter returns a status value from the call. For example:
Set val=##class(Ens.Director).GetHostSettingValue("myProd||myOp","QSize",.tSC)
If the returned status value is not $$$OK, the specified combination of production name (myProd) and configuration item name (myOp) could not be found, or a setting of the specified name (QSize) was not found in the configuration for that specified production and configuration item.
Returns an array containing the values of all production settings from the named production. The array is subscripted by setting name. The return value for this method is a status value. If the status value is not $$$OK, the specified production could not be found.
Set tSC=##class(Ens.Director).GetProductionSettings("myProd",.tSettings)
Returns the value of a named production setting from the named production. The third output parameter returns a status value from the call. If this status value is not $$$OK, the specified production could not be found, or a setting of the specified name was not found in the configuration for the specified production.
Set val=##class(Ens.Director).GetProductionSettingValue("prod","set",.tSC)
Ens.DirectorOpens in a new tab provides many class methods, including many intended for use only by the Ensemble internal framework. InterSystems recommends that you use only the Ens.DirectorOpens in a new tab methods documented in this book, and only as documented.
Invoking a Business Service Directly
There are times when you want to invoke a business service directly, from a job that has been created by some other mechanism, such as a language binding, Caché Server Pages, SOAP, or a routine invoked from the operating system level. You can do so only if the value of the ADAPTER class parameter is null; this type of business service is called an adapterless business service.
For a business service to work, you must create an instance of the business service class. You cannot create this instance by calling the %New() method. Instead, you must use the method CreateBusinessService() of Ens.DirectorOpens in a new tab. For example:
Set tSC = ##class(Ens.Director).CreateBusinessService("MyService",.tService)
An Ensemble production does not allocate a job for this business service at production startup; it assumes a Pool Size setting of 0.
The CreateBusinessService() method does the following:
-
It makes sure that a production is running and that the production defines the given business service.
-
It makes sure that the given business service is currently enabled.
-
It resolves the configuration name of the business service and instantiates the correct business service object using the correct configuration values (a production may define many business services using the same business service class but with different names and settings).
If the CreateBusinessService() method succeeds, it returns, by reference, an instance of the business service class. You can then invoke its ProcessInput() method directly. You must provide the ProcessInput() method with an instance of the input object it expects. For example:
If ($IsObject(tService)) {
Set input = ##class(MyObject).%New()
Set input.Value = 22
Set tSC = tService.ProcessInput(input,.output)
}
If you call the CreateBusinessService() method and the business service calls an InProc business operation that requires access to credentials, see the compatibility note “New Global and Database Used to Store Credentials Passwords” in the Ensemble Release Notes.
Ens.DirectorOpens in a new tab provides many class methods, including many intended for use only by the Ensemble internal framework. InterSystems recommends that you use only the Ens.DirectorOpens in a new tab methods documented in this book, and only as documented.
Creating or Subclassing Inbound Adapters
This section describes how to create or subclass an inbound adapter.
Introduction to Inbound Adapters
An inbound adapter is responsible for receiving and validating requests from external systems.
Inbound adapter classes work in conjunction with a business service classes. In general, the inbound adapter contains general-purpose, reusable code while the business service contains production-specific code (such as special validation logic). Typically you implement inbound adapter classes using one of Ensemble’s built-in adapter classes. The following figure shows how a production accepts incoming requests:
![generated description: business service](images/egdv_business_service.png)
In general, when an external application makes a request for a certain action to be performed, the request comes into Ensemble via an inbound adapter, as shown in the previous figure. The requesting application is called a “client” application, because it has asked the production to do something. This application is a “client” of the production. The featured element at this step is the inbound adapter. This is a piece of code that “adapts” the client’s native request format to a one that is understandable to the production. Each application that makes requests of a production must have its own inbound adapter. No change to the client application code is needed, because the adapter handles calls that are already native to the client application.
Defining an Inbound Adapter
To create an inbound adapter class, create a class as follows:
-
Your class must extend Ens.InboundAdapterOpens in a new tab (or a subclass).
-
Your class must implement the OnTask() method, as described in “Implementing the OnTask() Method.”
-
Your class can define settings. See “Adding and Removing Settings,” earlier in this book.
-
Your class can implement any or all of the startup and teardown methods. See “Overriding Start and Stop Behavior,” in this chapter.
-
Your class can include Ensemble credentials. See “Including Credentials in an Adapter Class,” in this chapter.
-
Your class can contain methods to accomplish work internal to itself.
The following shows an example:
Class MyProduction.InboundAdapter Extends Ens.InboundAdapter
{
Parameter SETTINGS = "IPAddress,TimeOut";
Property IPAddress As %String(MAXLEN=100);
Property TimeOut As %Integer(MINVAL=0, MAXVAL=10);
Property Counter As %Integer;
Method OnTask() As %Status
{
#; First, receive a message (note, timeout is in ms)
Set msg = ..ReceiveMessage(..CallInterval*1000,.tSC)
If ($IsObject(msg)) {
Set tSC=..BusinessHost.ProcessInput(msg)
}
Quit tSC
}
}
Studio provides a wizard that you can use to create a class stub similar to the preceding. To access this wizard, click File —> New and then click the Production tab. Then click Adapter and click OK.
Implementing the OnTask() Method
The OnTask() method is where the actual work of the inbound adapter takes place. This method is intended to do the following things:
-
Check for an incoming event. The inbound adapter can do this in many different ways: For example, it could wait for an incoming I/O event (such as reading from a TCP socket), or it could periodically poll for the existence of external data (such as a file).
Most prebuilt inbound adapters have a setting called CallInterval that controls the time interval between calls to OnTask(). You could use this approach as well.
-
Package the information from this event into an object of the type expected by the business service class.
-
Call the ProcessInput() method of the business service object.
-
If necessary, send an acknowledgment back to external system that the event was received.
-
If there is more input data, OnTask() can do either of the following:
-
Call ProcessInput() repeatedly until all the data is retrieved.
-
Call ProcessInput() only once per CallInterval, even if multiple input events exist.
-
When designing an inbound adapter, it is important to keep in mind that the OnTask() method must periodically return control to the business service; that is, the OnTask() method must not wait indefinitely for an incoming event. It should, instead, wait for some period of time (say 10 seconds) and return control to the business service. The reason for this is that the business service object must periodically check for events from within Ensemble, such as notification that the production is being shut down.
Conversely, it is also important that the OnTask() method waits for events efficiently—sitting in a tight loop polling for events wastes CPU cycles and slows down an entire production. When an OnTask() method needs to wait, it should wait in such a way as to let its process go to sleep (such as waiting on an I/O event, or using the Hang command).
If your class is a subclass of an Ensemble adapter, then it probably implements the OnTask() method; therefore, your subclass might need to override a different method as specified by the inbound adapter class.
Creating or Subclassing Outbound Adapters
This section describes how to create or subclass an outbound adapter.
Introduction to Outbound Adapters
An outbound adapter is responsible for sending requests to external systems. The following figure shows how a production relays outgoing requests.
![generated description: business operation](images/egdv_business_operation.png)
The outbound adapter is a piece of code that “adapts” the native programming interface of an external application or external database into a form that is understandable to an Ensemble production. Each external application or database that serves a production by means of a business operation must have its own outbound adapter. However, not every method in the external application or database needs to be mapped to the outbound adapter; only those operations that the production requires. As with inbound adapters, no change to the external application itself is needed to create an outbound adapter. And, the adapter itself is conceptually simple: It relays requests, responses, and data between the production and a specific application or database outside the production.
Outbound adapter classes work in conjunction with a business operation classes. In general, the outbound adapter contains general-purpose, reusable code while the business operation will contain production-specific code (such as special processing logic). Typically you will implement your outbound adapter classes using one of Ensemble’s built-in adapter classes.
Defining an Outbound Adapter
To create an outbound adapter class, create a class as follows:
-
Your class must extend Ens.OutboundAdapterOpens in a new tab (or a subclass).
-
Your class must define one or more methods for the corresponding business operation to invoke. Every outbound adapter is free to define its own API (set of methods) for use by the associated business operation classes.
-
Your class can define settings. See “Adding and Removing Settings,” earlier in this book.
-
Your class can implement any or all of the startup and teardown methods. See “Overriding Start and Stop Behavior,” in this chapter.
-
Your class can include Ensemble credentials. See “Including Credentials in an Adapter Class,” in this chapter.
-
Your class can contain methods to accomplish work internal to itself.
Including Credentials in an Adapter Class
To include Ensemble credentials in an adapter class, do the following in the class definition:
-
Include a setting named Credentials.
-
Define a method called CredentialsSet() that uses the value of the Credentials setting as a key to look up the username and password in the Credentials table. It then instantiates a credentials object that contains the username and password.
Overriding Ensemble Credentials
While the Ensemble credentials system centralizes management and keeps login data out of source code, sometimes you need to write code that gets credentials from another source. For example, your code might retrieve a username and password from a web form or cookie, and then use them with the HTTP outbound adapter to connect to some other site.
The way to handle this is in your business service or business operation code, do both of the following, before calling any adapter methods:
-
Provide code that instantiates a credentials object and assigns username and password values to it
-
Do not subsequently set the adapter Credentials property or call the adapter CredentialsSet() method, or the values may be reset.
For example:
If ..Adapter.Credentials="" {
Set ..Adapter.%CredentialsObj=##class(Ens.Config.Credentials).%New()
}
Set ..Adapter.%CredentialsObj.Username = tUsername
Set ..Adapter.%CredentialsObj.Password = tPassword
Code such as this provides a credentials object that the EnsLib.HTTP.OutboundAdapterOpens in a new tab can use, but the values inside the object do not come from the Credentials table.
Overriding Start and Stop Behavior
Ensemble provides a set of callback methods that you can override in order to add custom processing at start and stop times during the life cycle of the production, its business hosts, or its adapters. By default, these methods do nothing.
Callbacks in the Production Class
If you have code that must execute before a production starts up, but that requires the Ensemble framework to be running before it can execute, you must override the OnStart() method in the production class. Place these code statements in OnStart() so that they execute in the proper sequence: that is, after Ensemble has started, but before the production begins accepting requests. The OnStop() method is also available to perform a set of tasks before the production finishes shutting down.
Callbacks in Business Host Classes
Each business host — business service, business process, or business operation — is a subclass of Ens.HostOpens in a new tab. In any of these classes you may override the OnProductionStart() method to provide code statements that you want Ensemble to execute on behalf of this host at production startup time. You can also implement the OnProductionStop() method.
For example, if your production requires different initial settings for property values, set the value in the OnInit() method of the business operation. For example, to change the initial setting of the LineTerminator property to depend on the operating system:
Method OnInit() As %Status
{
Set ..Adapter.LineTerminator="$Select($$$isUNIX:$C(10),1:$C(13,10))"
Quit $$$OK
}
Callbacks in Adapter Classes
An adapter class may override the OnInit() method. This method is called after the adapter object has been created and its configurable property values have been set. The OnInit() method provides a way for an adapter to perform any special setup actions.
For example, the following OnInit() method establishes a connection to a device when the adapter is started — assuming that this adapter also implements a ConnectToDevice() method:
Method OnInit() As %Status
{
// Establish a connection to the input device
Set tSC = ..ConnectToDevice()
Quit tSC
}
An adapter class can also override the OnTearDown() method. This method is called during shutdown before the adapter object is destroyed. The OnTearDown() method provides a way for an adapter to perform any special cleanup actions.
For example, the following OnTearDown() method closes a connection to a device when the adapter is stopped, assuming that this adapter also implements a method named CloseDevice():
Method OnTearDown() As %Status
{
// close the input device
Set tSC = ..CloseDevice()
Quit tSC
}
Programmatically Working with Lookup Tables
Ensemble provides the utility function called Lookup() so that you can easily perform a table lookup from a business rule or DTL data transformation. This function works only after you have created at least one lookup table and have populated it with appropriate data.
For information on defining lookup tables, see “Defining Data Lookup Tables” in Configuring Ensemble Productions.
If you need more direct manipulation of lookup tables than the Management Portal provides, use the Ens.Util.LookupTableOpens in a new tab class. This class exposes lookup tables to access via objects or SQL. Additionally, it provides class methods to clear tables, export data as XML, and import data from XML.
Ens.Util.LookupTableOpens in a new tab provides the following string properties:
Name of the lookup table, up to 255 characters. You can view the lookup tables defined in a namespace by selecting Ensemble, Configure, and Data Lookup Tables in the Ensemble portal and then selecting Open.
Key for the entry within the lookup table, up to 255 characters. This is the value from the Key field on the Lookup Tables page (select Ensemble > Configure > Data Lookup Tables).
Value associated with this key in the lookup table, up to 32000 characters. This is the value from the Value field on the Lookup Tables page.
A sample SQL query might be:
SELECT KeyName,DataValue FROM Ens_Util.LookupTable WHERE TableName = 'myTab'
Ens.Util.LookupTableOpens in a new tab also provides the following class methods:
Deletes the contents of the specified lookup table.
do ##class(Ens.Util.LookupTable).%ClearTable("myTab")
Imports lookup table data from the specified XML file. For the import to be successful, the file must use the same XML format as that provided by the %Export() method of this class.
do ##class(Ens.Util.LookupTable).%Import("myFile.xml")
Exports lookup table data to the specified XML file. If the file exists, Ensemble overwrites it with new data. If the file does not already exist, Ensemble creates it. The following example exports only the contents of the specified lookup table, myTab:
do ##class(Ens.Util.LookupTable).%Export("myFile.xml","myTab")
The following example exports the contents of all lookup tables in the namespace:
do ##class(Ens.Util.LookupTable).%Export("myFile.xml")
The resulting XML file looks like the following example. Note that all entries, in all tables, appear as sibling <entry> elements inside a single <lookupTable> element.
<?xml version="1.0"?>
<lookupTable>
<entry table="myOtherTab" key="myKeyA">aaaaaa</entry>
<entry table="myOtherTab" key="myKeyB">bbbbbbbbb</entry>
<entry table="myTab" key="myKey1">1111</entry>
<entry table="myTab" key="myKey2">22222</entry>
<entry table="myTab" key="myKey3">333333</entry>
</lookupTable>
For each <entry>, the table attribute identifies the table that contains the entry. The key attribute gives the name of the key. The text contents of the <entry> element provide the entry’s value.
In addition to the XML format described above, you can use the SQL Import Wizard to import comma-separated value (CSV) files that list tables and keys.
Defining a Custom Archive Manager
For Ensemble, the Management Portal provides a tool called the Archive Manager; this is described in Managing Ensemble. You can define and use a custom Archive Manager. To do so, create a class as follows:
-
It can use Ens.Archive.ManagerOpens in a new tab as a superclass.
-
It must define the DoArchive() method, which has the following signature:
ClassMethod DoArchive() As %Status
An alternative option is to use the Enterprise Message Bank, which enables you to archive messages from multiple productions. For an overview, see “Defining the Enterprise Message Bank,” earlier in this book.