Caché Specialized System Tools and Utilities
Customizing the Caché System
[Home] [Back] [Next]
InterSystems: The power behind what matters   
Class Reference   
Search:    

This chapter discusses several ways to customize and extend a standard Caché installation.

Using System Classes for National Language Support
Modern applications are often designed so that they can adapt to various languages and regions without engineering changes. This process is called internationalization. The process of adapting an application to a specific region or language by adding specific components for that purpose is localization.
The set of parameters that defines the user language, country and any other, special variant preferences is a locale. Locales specify the conventions for the input, output and processing of data. These are such things as
A locale often is identified by noting the language in use and its geographic region (or other variation). These are usually given by the International Standards Organization (ISO) abbreviations for language and location. For example, en-us can represent the conventions of the English language as it is used in the United States, and en-gb as English is used in Great Britain.
Note:
The Caché instances on all members of a mirror must have the same locale and collation. See the Mirroring chapter of the Caché High Availability Guide for more information.
The %SYS.NLS Classes
Caché supports localization via classes in the package %SYS.NLS (NLS refers to National Language Support). These classes contain the information Caché needs to adapt an internationalized program to its runtime circumstances. This section summarizes your options; for additional detail, see the class documentation for each class.
Note:
Using any of these classes, an application can obtain the values currently set for the system or the process. Changing the values associated with the process takes effect immediately. To change the system settings, your application must define a new locale with the appropriate values and direct Caché to start using the new locale.
%SYS.NLS.Locale
The properties in %SYS.NLS.Locale contain information about the current locale that you might need to consult. Changing any of them will not affect any behavior of the system.
%SYS.NLS.Device
The class %SYS.NLS.Device contains some properties for the current device, not necessarily the device that was current when the object was instantiated.
Usually, the properties for a specific device are set when the device is opened. This guarantees that the correct translations will be used. It is possible to change the translation table once the device is open by changing the XLTTable property in the process instance of this class, but this is not recommended without a solid reason for doing so.
Other properties in %SYS.NLS.Device enable you to handle errors that occur during translations. By default, when a character cannot be handled by the current table, no error is triggered and the offending character is translated as a question mark (?). This character, called the replacement value or replacement string can be changed to any other string. Furthermore, instead of silently translating undefined characters, it is possible to issue an error. This behavior is called the default action, and the possible choices are:
There are separate properties for the input and output operations in the properties of this class:
%SYS.NLS.Format
The class %SYS.NLS.Format contains properties that affect the behavior of $ZDATE() and related functions. These properties are inherited from the values defined for the current locale but can be altered at the process level without affecting other users. The properties DateSeparator and TimeSeparator, for example, hold the characters that separate date and time items respectively.
The documentation for $ZDATE, $ZDATEH, and $FNUMBER describes the effect of changing these values.
Locale Property
The Locale property in the class %SYS.NLS.Format allows control of the “look” of values in the current process. For example:
The property can be changed after the object is instantiated or by passing the desired locale to the %New() method as in the following:
 Set fmt = ##class(%SYS.NLS.Format).%New("jpnw")
These changes affect only the current process.
%SYS.NLS.Table
The class %SYS.NLS.Table can instantiate objects that reflect either the system default or the current process settings for the various categories of tables. A table is the basic NLS mechanism that allows application data to be accepted as input, ordered, and displayed in the format appropriate to the specified locale. As with %SYS.NLS.Locale, changing any property of a system object will not affect the system. However, changing a property from a process object will cause the associated behavior to change immediately.
NLS tables can be classified into I/O and Internal tables. Each table type has its own set of related data:
I/O Tables
These tables translate between the basic underlying character set supported by the current locale in which the systems is operating and a foreign character set supported by some entity outside Caché. The locale character set might be, for example, Latin2 (more properly known as ISO 8859-2) and the foreign character set might be UTF-8, generally used to communicate with the Terminal. Thus, on output, a table like Latin2–to-UTF8 would be used and, on input, a reverse mapping table would be needed, UTF8–to-Latin2.
Although there are two tables involved here (one for input and another for output), these tables usually complement one another. For simplicity, when speaking of locale definitions and system defaults, Caché uses a single name for a pair of I/O tables. This name is usually the name of the foreign character set, with the tacit assumption that the other half is the locale character set. However, when creating custom tables, any name that conveys the meaning of the exchange can be chosen.
I/O tables are used in devices; in this case, the word device refers to any interface where Caché meets the external world and where translation is needed, including the process and system call interfaces.
Internal Tables
The internal tables also map strings of characters from the current local character set to some other value, but they are not intended to be used in communication with the external world. The internal tables identify characters that are part of:
Note:
The list of available collations in any version of Caché is fixed. If your needs are not met by an existing collation, please contact the InterSystems Worldwide Support Center for assistance.
Examples Using %SYS.NLS
Important:
These examples are all executable but none have a RunIt button, because they manipulate process-default values for the current locale. Also, many require administrative privileges and/or write access to the %SYS namespace. If you wish to execute them, please run them in a separate process, such as the InterSystems Terminal facility (Windows), or via a TCP/IP connection, and with the appropriate privileges.
Display Current Locale Information
This example displays information about the current system locale:
  Set Info = ##class(%SYS.NLS.Locale).%New()
  Set Items = "Name" _
              "/Description" _
              "/Country" _
              "/CountryAbbr" _
              "/Language" _
              "/LanguageAbbr" _
              "/Currency" _
              "/CharacterSet"
  
  Write !
  For i = 1 : 1 : $LENGTH(Items, "/")
  {
    Set Item = $PIECE(Items, "/", i)
    Write $JUSTIFY(Item, 15),": ", $PROPERTY(Info, Item), !
  }
Display Available Locales
This example displays information about the available locales:
  Znspace "%SYS"
  Set locales = ##class(%Library.ResultSet).%New("Config.NLS.Locales:List")
  If $IsObject(locales) {
    Set locales.RuntimeMode = 1
    Set sc = locales.Execute("*")
    If $SYSTEM.Status.IsOK(sc) {
      Write !
      While locales.Next() {
        Write locales.Data("Name"), " - ", locales.Data("Description"), !
      }
    }
  }
Display System and Process Table Data
This should display the same values for the system and process tables unless some properties have been externally altered before running this example.
  Set IOTables = "Process" _
                 "/CacheTerminal" _
                 "/OtherTerminal" _
                 "/File" _
                 "/Magtape" _
                 "/TCPIP" _
                 "/DSMDDP" _
                 "/DTMDCP" _
                 "/SystemCall" _
                 "/Printer"
  Set IntTables = "PatternMatch" _
                  "/Identifier" _
                  "/Uppercase" _
                  "/Lowercase" _
                  "/Titlecase" _
                  "/Collation" _
                  "/XYAction"
  
  // iterate over the systems, and then the process data
  For Type = "System", "Process"
  {
    Write !
    Set Table = ##class(%SYS.NLS.Table).%New(Type)
    Write "Type: ", Type, !
    
    Write "I/O Tables", !
    For i = 1 : 1 : $LENGTH(IOTables, "/")
    {
      Set PropName = $PIECE(IOTables, "/", i)
      Write $JUSTIFY(PropName, 15), ": ", $PROPERTY(Table, PropName), !
    }
    
    Write "Internal Tables", !
    For i = 1 : 1 : $LENGTH(IntTables, "/")
    {
      Set PropName = $PIECE(IntTables, "/", i)
      Write $JUSTIFY(PropName, 15), ": ", $PROPERTY(Table, PropName), !
    }
  }
Changing Date and Time Displays
The %SYS.NLS.Format class contains the properties DateSeparator and TimeSeparator, for example, hold the characters used to separate the components of date and time items respectively. In the United States default locale, enu8 (or enuw for Unicode systems), these are the slash character (/) and the colon (:), respectively. The following example shows how these may be altered:
  // display the current defaults
  // date is 10 April 2005
  // time is 6 minutes 40 seconds after 11 in the morning
  Write $ZDATE("60000,40000"), !
  
  // now change the separators and display it again
  Set fmt = ##class(%SYS.NLS.Format).%New()
  Set fmt.DateSeparator = "_"
  Set fmt.TimeSeparator = "^"
  Write !, $ZDATE("60000,40000")
This following example changes the month names to successive letters of the alphabet (for demonstration purposes). To do this, it sets the property MonthName to a space-separated list of the month names. Note that the list starts with a space:
  // get the format class instance
  Set fmt = ##class(%SYS.NLS.Format).%New()
  
  // define the month names
  Set Names = " AAA BBB CCC DDD EEE FFF GGG HHH III JJJ KKK LLL"
  Set fmt.MonthAbbr = Names
  Set rtn = ##class(%SYS.NLS.Format).SetFormatItem("DATEFORMAT", 2) 
  
  // show the result
  Write $ZDATE(60000, 2)
Changing the Way Numbers Are Displayed
Some properties in %SYS.NLS.Format control how numbers are interpreted by $Number(). In English locales, the decimal point is used to separate the integer from the fractional part of a number, and a comma is used to separate groups of 3 digits. This too can be altered:
  // give the baseline display
  Write $Number("123,456.78"), !
  
  Set fmt = ##class(%SYS.NLS.Format).%New()
  // use "/" for groups of digits
  Set fmt.NumericGroupSeparator = "."
  
  // group digits in blocks of 4
  Set fmt.NumericGroupSize = 4
  
  // use ":" for separating integer and fractional parts
  Set fmt.DecimalSeparator = ","
  
  // try interpreting again
  Write $Number("12.3456,78"), !
Setting the Translation for a File
The following shows that an application can control the representation of data written to a file.
  // show the process default translation (RAW, no translation performed)
  Set Tbl = ##class(%SYS.NLS.Table).%New("Process")
  Write "Process default translation: ", Tbl.File, !
  
  // create and open a temporary file
  // use XML for the translation
  Set TempName = ##class(%Library.File).TempFilename("log")
  Set TempFile = ##class(%Library.File).%New(TempName)
  Do TempFile.Open("WSNK\XML\")
  Write "Temp file: ", TempFile.CanonicalNameGet(), !
  
  // write a few characters to show the translation
  // then close it
  Do TempFile.WriteLine(("--" _ $CHAR(38) _ "--"))
  Do TempFile.Close()
  
  // now re-open it in raw mode and show content
  Do TempFile.Open("RSK\RAW\")
  Do TempFile.Rewind()
  Set MaxChars = 50
  Set Line = TempFile.Read(.MaxChars)
  Write "Contents: """, Line, """", !
  
  // finish
  Do TempFile.Close()
  Do ##class(%Library.File).Delete(TempName)
  Set TempFile = "" 
For more information on translation tables, see the section on “Three-Parameter Form: Encoding Translation” in the documentation for the $ZCONVERT function.
The Config.NLS Classes
Unlike %SYS.NLS, which is available everywhere and is intended for general use, the classes in Config.NLS can be used only in the %SYS namespace and only by a user with administrative privileges. Normally, administrators who need to create custom locales and tables would use the NLS pages in the Management Portal. Only users with very special requirements should need to use Config.NLS.
There are three classes in package Config.NLS:
The main reason for having separate Tables and SubTables classes is to avoid duplication of data. It is possible to have Tables for different character sets that happen to share the same mappings and thus the same SubTable. Also, the classes in Tables define a default action and a replacement value (see description of these properties in %SYS.NLS above). Therefore, it is possible to have separate Tables in which these attributes are different even though they share the same SubTable. This flexibility adds some complexity in managing the correct relationships between Tables and SubTables, but the gains make it worthwhile. The separation of Tables from SubTables is kept hidden from users in the Management Portal and the %SYS.NLS classes, where all the housekeeping is done. However, when working with Config.NLS this needs to be done explicitly.
Conventions for Naming User-Defined Locales and Tables
To differentiate your custom items from the system items, and to simply upgrades, use a y at the start of the name of your items; for example: XLT-yEBCDIC-Latin1 and XLT-Latin1-yEBCDIC.
Caution:
User-defined tables, sub-tables and locales that do not follow this convention may be deleted during a system upgrade. The way to avoid this is to export user-defined tables and locales to XML files and re-import them after the upgrade.
When a custom SubTable is created from a copy of some InterSystems SubTable, the utilities that perform this task automatically use the same name and append a numeric suffix. Thus, copies of the Latin2-to-Unicode SubTable would be named XLT-Latin2-Unicode.0001 and XLT-Unicode-Latin2.0001, and so on.
Examples Using Config.NLS
This section presents the following examples:
Listing the Available Locales
This example uses a query to obtain a list of the available locale identifiers and descriptions. When Caché is installed, only the locales appropriate to the system are made available for use — 8-bit locales for systems that only support 8-bit characters and Unicode locales for systems that support multibyte characters.
  // use the query in Config.NLS to get the locales
  ZNspace "%SYS"
  Set Query = ##class(%Library.ResultSet).%New("Config.NLS.Locales:List")
  Set code = Query.Execute("*")
  If (##class(%SYSTEM.Status).IsError(code))
  {
    Do ##class(%SYSTEM.Status).DisplayError(Code)
    Quit
  }
  
  // display each of them in turn
  Write "Available locales and descriptions", !
  While (Query.Next(.code))
  {
    If (##class(%SYSTEM.Status).IsError(code))
    {
      Do ##class(%SYSTEM.Status).DisplayError(Code)
      Quit
    }
    Write Query.Get("Name"), ": ", Query.Get("Description"), !
  }
 
Listing the Tables in a Specific Locale
The following example shows the tables that make up the Unicode locale for United States English (if it is available).
  ZNspace "%SYS"
  
  // establish the locale identifier, try
  // United States - English - Unicode
  // United States - English - 8-bit
  Set Loc = "enuw"
  Do ##class(Config.NLS.Locales).Exists(Loc, .Ref, .Code)
  If (##class(%SYSTEM.Status).IsError(Code))
  {
    Set Loc = "enu8"
    Do ##class(Config.NLS.Locales).Exists(Loc, .Ref, .Code)
    If (##class(%SYSTEM.Status).IsError(Code))
    {
      Do ##class(%SYSTEM.Status).DisplayError(Code)
      Quit
    }
  }
  
  // get the local array of table names
  Write "Tables for locale: ", Loc, !
  Do Ref.GetTables(.Tables)
  Set Type = $ORDER(Tables(""))
  While (Type '= "")
  {
    Set Name = $ORDER(Tables(Type, ""))
    While (Name '= "")
    {
      Set Mod = $ORDER(Tables(Type, Name, ""))
      While (Mod '= "")
      {
        Write Type, " - ", Name, " - ", Mod, !
        Set Mod = $ORDER(Tables(Type, Name, Mod))
      }
      Set Name = $ORDER(Tables(Type, Name))
    }
    Set Type = $ORDER(Tables(Type))
  }
Creating a Custom Locale
This example will provide a template for creating a custom locale with a custom table. The custom table will translate between EBCDIC (the common form used in the US) and Latin-1 (ISO-8859–1). For more details, see the documentation for the respective classes.
As for any other table, first we need to get the definition for the character mappings. For this example we are using the data file from the web site http://source.icu-project.org (International Components for Unicode). The relevant data file is a text file with comment lines starting with a pound sign (#) and then a series of translation definition lines of the form:
<Uuuuu>  \xee |0
A small excerpt of the file looks like:
#
#UNICODE EBCDIC_US
#_______ _________
<U0000>  \x00 |0
<U0001>  \x01 |0
<U0002>  \x02 |0
<U0003>  \x03 |0
<U0004>  \x37 |0
<U0005>  \x2D |0
...
The lines indicate that Unicode character Uaaaa maps to EBCDIC character \xbb (where aaaa and bb are expressed in hexadecimal). We assume that the table is reversible and that EBCDIC character \xbb maps back to Unicode character Uaaaa. This allows us to create both sides (that is, EBCDIC-to-Latin1 and Latin1-to-EBCDIC) from the same data file in a single scan. Because the Unicode range is just from 0 to 255, this is actually a Latin-1 table.
The process first creates the SubTable object, then the Table, and finally the Locale. For the first step, the process creates two SubTables objects, initializes their Name and Type properties, and then fills in the FromTo mapping array with data read from the definition file.
SubTable names take the form, Type–FromEncoding–ToEncoding. The Type for regular I/O translations is “XLT” and so the SubTable names will be XLT-yEBCDIC-Latin1 and XLT-yLatin1-EBCDIC.
The following code creates the SubTables objects. In a real world program, the code would perform a number of consistency checks that omitted here for the sake of clarity. This example deletes an existing previous versions of the same objects (SubTables, Tables and Locales) so that you can run the example multiple times. More properly, you should check for the existence of previous objects using the class method Exists() and take a different action if they are already present.
  // Names for the new SubTables (save for later)
  Set nam1 = "XLT-Latin1-yEBCDIC"
  Set nam2 = "XLT-yEBCDIC-Latin1"

  // Delete existing SubTables instances with same ids
  Do ##class(Config.NLS.SubTables).Delete(nam1)
  Do ##class(Config.NLS.SubTables).Delete(nam2)

  // Create two SubTable objects
  Set sub1 = ##class(Config.NLS.SubTables).%New()
  Set sub2 = ##class(Config.NLS.SubTables).%New()

  // Set Name and Description
  Set sub1.Name = nam1
  Set sub1.Description = "ICU Latin-1->EBCDIC sub-table"
  Set sub2.Name = nam2
  Set sub2.Description = "ICU EBCDIC ->Latin-1 sub-table"
The SubTables object contains a property, type, that is a small integer indicating whether we are dealing with a multibyte translation or not. This example sets type to zero indicating a single-byte mapping. The mapping is initialized so that code points (characters) not defined in the data file are mapped to themselves.
  // Set Type (single-to-single)
  Set sub1.Type = 0
  Set sub2.Type = 0
  
  // Initialize FromTo arrays
  For i = 0 : 1 : 255
  {
    Do sub1.FromTo.SetAt(i, i)
    Do sub2.FromTo.SetAt(i, i)
  }
Next the application reads the file. Definitions in the file override those set as the default mapping. The function $ZHEX() converts the codes from hexadecimal to decimal.
  // Assume file is in the mgr directory
  Set file = "glibc-EBCDIC_US-2.1.2.ucm"

  // Set EOF exit trap
  Set $ZTRAP = "EOF"
  
  // Make that file the default device
  Open file
  Use file
  For
  {
    Read x
    If x?1"<U"4AN1">".E
    {
      Set uni = $ZHEX($E(x,3,6)),ebcdic = $ZHEX($E(x,12,13))
      Do sub1.FromTo.SetAt(ebcdic,uni)
      Do sub2.FromTo.SetAt(uni,ebcdic)
     }
  }

EOF  // No further data
  Set $ZT = ""
  Close file

  // Save SubTable objects
  Do sub1.%Save()
  Do sub2.%Save()
The character mappings are now complete. The next step is to create the Table objects that reference the SubTables objects just defined. Table objects are really descriptors for the SubTables and have only a few properties. The following code makes the connection between the two:
  // Delete existing Tables instances with same ids
  Do ##class(Config.NLS.SubTables).Delete("XLT", "Latin1", "yEBCDIC")
  Do ##class(Config.NLS.SubTables).Delete("XLT", "yEBCDIC", "Latin1")

  // Create two Table objects
  Set tab1 = ##class(Config.NLS.Tables).%New()
  Set tab2 = ##class(Config.NLS.Tables).%New()

  // Set description
  Set tab1.Description = "ICU loaded Latin-1 -> EBCDIC table"
  Set tab2.Description = "ICU generated EBCDIC -> Latin-1 table"

  // Set From/To encodings
  Set tab1.NameFrom = "Latin1"
  Set tab1.NameTo = "yEBCDIC"
  Set tab2.NameFrom = "yEBCDIC"
  Set tab2.NameTo = "Latin1"

  // Set SubTable
  Set tab1.SubTableName = nam1
  Set tab2.SubTableName = nam2

  // Set Type
  Set tab1.Type = "XLT"
  Set tab2.Type = "XLT"

  // Set Default Action
  // 1 = Replace with replacement value
  Set tab1.XLTDefaultAction = 1
  Set tab2.XLTDefaultAction = 1

  // Set Replacement value of "?"
  Set tab1.XLTReplacementValue = $ASCII("?")
  Set tab2.XLTReplacementValue = $ASCII("?")

  // Set Reversibility
  // 1 = Reversible
  // 2 = Generated
  Set tab1.XLTReversibility = 1
  Set tab2.XLTReversibility = 2

  // Set Translation Type
  // 0 = non-modal to non-modal
  Set tab1.XLTType = 0
  Set tab2.XLTType = 0

  // Save Table objects
  Do tab1.%Save()
  Do tab2.%Save()
With the Tables defined, the last step of the construction is to define a locale object that will incorporate the new tables. The application creates an empty Locale object and fills in each of the properties as was done for the Tables and SubTables. A Locale, however, is bigger and more complex. The easiest way to make a simple change like this is to copy an existing locale and change only what we need. This process uses enu8 as the source locale and names the new one, yen8. The initial y makes it clear this is a custom locale and should not be deleted on upgrades.
  // Delete existing Locales instance with the same id
  Do ##class(Config.NLS.Locales).Delete("yen8")
  
  // Open source locale
  Set oldloc = ##class(Config.NLS.Locales).%OpenId("enu8")
  
  // Create clone
  Set newloc = oldloc.%ConstructClone()
  
  // Set new Name and Description
  Set newloc.Name = "yen8"
  Set newloc.Description = "New locale with EBCDIC table"
With the locale in place, the process now adds the EBCDIC table to the list of I/O tables that are loaded at startup. This is done by inserting a node in the array property XLTTables, as follows:
XLTTables(<TableName>) = <components>
The following code adds the table to the list of available locales:
  // Add new table to locale
  Set component = $LISTBUILD("yEBCDIC", "Latin1", "Latin1", "yEBCDIC")
  Do newloc.XLTTables.SetAt(component, "EBCDIC")
If this locale will be frequently used, for example, for reading with EBCDIC magnetic tapes, the following code will set it as the default for this class of devices:
  // Set default for Magnetic Tapes
  Set newloc.TranMagTape = "EBCDIC"
  
  // Save the changes
  Do newloc.%Save()
Before the locale is usable by Caché, it must be compiled into its internal form. This is also sometimes called validating the locale. The IsValid() class method does a detailed analysis and returns two arrays, one for errors and one for warnings, with human-readable messages if the locale is not properly defined.
  // Check locale consistency
  If '##class(Config.NLS.Locales).IsValid("yen8", .Errors, .Warns)
  {
    Write !,"Errors: "
    ZWrite Errors
    Write !,"Warnings: "
    ZWrite Warns
    Quit
  }
  
  // Compile new locale
  Set status = ##class(Config.NLS.Locales).Compile("yen8")
  If (##class(%SYSTEM.Status).IsError(status))
  {
    Do $System.OBJ.DisplayError(status)
  }
  Else 
  { 
    Write !,"Locale yen8 successfully created."
  }
Using %Library.GlobalEdit to Set the Collation for a Global
The collation of newly created Caché globals is automatically set to the default collation of the database in which the global is created. The databases created by Caché installation are all set to the Caché standard collation, except USER, which is set to the default collation for the locale with which Caché is installed
After you create a database, you can edit its properties to change its default collation. You can select Caché standard, the default collation for the locale, or any other collation loaded in the instance. Once the default collation of the database is set, any globals created in this database are created with this default collation.
Caché also supports the ability to override this behavior and specify a custom collation for a global. To do this, use the Create() method in the class %Library.GlobalEdit supplying the collation desired:
  Set sc = ##class(%Library.GlobalEdit).Create(ns,
                                               global,
                                               collation,
                                               growthblk,
                                               ptrblock,
                                               keep,
                                               journal,
                                               .exists)
where:
In environments in which some globals require different collations from other globals, InterSystems recommends that you set up a database for each different collation, and that you add a global mapping within the namespace to map each global to the database with its required collation. This method allows mixed collations to be used without changing application code to specially use the Create() method call.
Supported Collations
The following are supported in Caché, for use in the collation argument of the CreateGlobal^%DM subroutine:
Note:
To see a similar list, including which collations have been loaded into the instance, open a Terminal window, change to the %SYS% namespace, and enter the command DO ^COLLATE.
Default Collation for the Installed Locale
The default collation for the locale of a new installation of Caché is always the most recent version of the collation, that is, the one with with the highest numeric suffix (as shown in the list in the previous section). For example, whn installing with a Spanish locale, the default collation is Spanish5. Older versions of the collation are supported for compatibility with existing databases.
When a Caché instance is upgraded, the default collation is preserved wven when the updated locale uses a new default. For example, if existing instance’s locale uses Finnish3 as the default collation and the updated instance would use Finnish4, the upgrade preserves Finnish3 as the default, but makes Finnish4 available for new globals and databases..
Customizing Start and Stop Behavior with ^%ZSTART and ^%ZSTOP Routines
Caché can execute your custom code when certain events occur. Two steps are required:
  1. Define the ^%ZSTART routine, the ^%ZSTOP routine, or both.
    In these routines, you can define subroutines to execute when the certain activities start or stop.
    ^%ZSTART and ^%ZSTOP must be defined in the %SYS namespace, although they can be mapped to a non-default database.
  2. Use the Management Portal to configure Caché to invoke the desired subroutines.
Specifically, if you define the routine ^%ZSTART and ^%ZSTOP and you include subroutines with specific names, the system automatically calls these subroutines when the activity is beginning or ending. The subroutine names are as follows:
For example, when a user logs in, the system automatically invokes LOGIN^%ZSTART, if that is defined and if you have used the Management Portal to enable this subroutine.
These subroutines are not intended to do complex calculations or run for long periods of time. Long calculations or potentially long operations like network accesses will delay the completion of the activity until the called routine returns. In this case, users may take a long (elapsed) time to login, or JOB throughput may be curtailed because they take too long to start.
Note:
These subroutines are called as part of normal Caché operation. This means that an external event which terminates Caché abnormally, such as a power failure, will not generate a call to ^%ZSTOP.
Note:
If a system implements ^%ZSTOP, and an application implements one or more $HALT routines, the ^%ZSTOP code is not executed until the last $HALT terminates with a HALT command. The failure of a $HALT routine to issue its own HALT command can prevent the ^%ZSTOP code from running.
Design Considerations
Because ^%ZSTART and ^%ZSTOP run in a somewhat restricted environment, the designer must keep several things in mind, namely:
Note:
On upgrades, Caché preserves only the %Z* routines that are mapped to the CACHESYS database, and if the .INT or .MAC code is available, recompiles them. Preservation of routines in other databases are the responsibility of the site administrator.
Enabling %ZSTART and %ZSTOP
Once the routines have been designed, developed, compiled, and are ready to be tested, individual entry points may be enabled through the Management Portal. Navigate to the [Home] > [Configuration] > [Startup Settings] page and edit the appropriate individual settings:
To deactivate one or more of the entry points, use the same procedure but change the value to false.
Debugging ^%ZSTART and ^%ZSTOP
The opportunities for debugging ^%ZSTART and ^%ZSTOP in their final environment are very limited. If an error occurs, errors are written to the operator console log, which is the current device while these routines are running. This file is cconsole.log and is found in the Manager’s directory.
The message indicates the reason for failure and location where the error was detected. This may be different from the place where the error in the program logic or flow actually occurred. The developer is expected to deduce the nature and location of the error from the information provided, or modify the routine so that future tests provide more evidence as to the nature of the error.
Removing %ZSTART and ^%ZSTOP
It is strongly recommended that you disable the entry point options via the Management Portal before deleting the routines. If the portal warns that a restart of Caché is needed for them to take effect, do this as well before proceeding. This guarantees that none of the entry points are being executed while they are being deleted.
Remember that ^%ZSTART and ^%ZSTOP (as well as any supporting routines) are stored persistently. To remove all traces of them, delete them through the Management Portal.
Example
The following example demonstrates a simple log for tracking system activity. It shows examples for ^%ZSTART and ^%ZSTOP, both of which use subroutines of a third example routine, ^%ZSSUtil, for convenience.
^%ZSSUtil Example
This routine has two public entry points. One writes a single line to the operator console log file. The other writes a list of name-value pairs to a local log file. Both files reside in the Manager’s directory, which is returned by the ManagerDirectory() method of the %Library.File class.
%ZSSUtil ;
    ; this routine packages a set of subroutines 
    ; used by the %ZSTART and %ZSTOP entry points
    ; 
    ; does not do anything if invoked directly
    quit
    
#Define Empty ""
#Define OprLog 1
    
WriteConsole(LineText) PUBLIC ;
    ; write the line to the console log
    ; by default the file cconsole.log in the MGR directory
    new SaveIO
    
    ; save the current device and open the operator console
    ; set up error handling to cope with errors
    ; there is little to do if an error happens
    set SaveIO = $IO
    set $ZTRAP = "WriteConsoleExit"
    open $$$OprLog
    use $$$OprLog
    ; we do not need an "!" for line termination
    ; because each WRITE statement becomes its 
    ; own console record (implicit end of line)
    write LineText
    ; restore the previous io device
    close $$$OprLog
    ; pick up here in case of an error
WriteConsoleExit ;
    set $ZTRAP = ""
    use SaveIO
    quit
    
WriteLog(rtnname, entryname, items) PUBLIC ;
    ; write entries into the log file
    ; the log is presumed to be open as 
    ; the default output device
    ; 
    ; rtnname: distinguishes between ZSTART & ZSTOP
    ; entryname: the name of the entry point we came from
    ; items: a $LIST of name-value pairs
    new ThisIO, ThisLog
    new i, DataString
    
    ; preserve the existing $IO device reference
    ; set up error handling to cope with errors
    ; there is little to do if an error happens
    set ThisIO = $IO
    set $ZTRAP = "WriteLogExit"

    ; construct the name of the file
    ; use the month and day as part of the name so that 
    ; it will create a separate log file each day
    set ThisLog = "ZSS"
                _ "-"
                _ $EXTRACT($ZDATE($HOROLOG, 3), 6, 10)
                _".log"
    
    ; and change $IO to point to our file
    open ThisLog:"AWS":0
    use ThisLog
    
    ; now loop over the items writing one line per item pair
    for i = 1 : 2 : $LISTLENGTH(items)
    {
        set DataString = $LISTGET(items, i, "*MISSING*")
        if ($LISTGET(items, (i + 1), $$$Empty) '= $$$Empty)
        {
            set DataString = DataString
                           _ ": "
                           _ $LISTGET(items, (i + 1))
        }
        write $ZDATETIME($HOROLOG, 3, 1),
              ?21, rtnname,
              ?28, entryname,
              ?35, DataString, !
    }
    
    ; stop using the log file and switch $IO back
    ; to the value saved on entry
    close $IO
    ; pick up here in case of an error
WriteLogExit ;
    set $ZTRAP = ""
    use ThisIO
    quit
Here is an description for each label:
^%ZSSUtil
This routine (as well as the others) begins with a QUIT command so that it is benign if invoked via
    do ^%ZSSUtil
The #DEFINE sequence cosmetically provides named constants in the body of the program. In this instance, it names the empty string and the device number of the operator console log.
WriteConsole^%ZSSUtil
The entry point is very simple. It is designed for low volume output, and as a minimally intrusive routine to use for debugging output.
It takes a single string as its argument and writes it to the operator console log. However, it must take care to preserve and restore the current $IO attachment across its call.
Each item sent to the device results in a separate record being written to the console log. Thus, the following results in four records being written.
    WRITE 1, 2, 3, !
The first three consist of a single digit and the fourth is a blank line. If multiple items are desired on a line, it is the responsibility of the caller to concatenate them into a string.
WriteLog^%ZSSUtil
This subroutine can be called by any entry point within ^%ZSTART or ^%ZSTOP. The first two arguments supply the information needed to report how the subroutine was launched. The third argument is a $LIST of name-value pairs to be written to the log.
This entry point first builds the name of the file it will use. To make log management easier, the name contains the month and day when the routine is entered. Therefore, calls to this subroutine create a new file whenever the local time crosses midnight. because the name is determined only at the time of the call. All the name-value pairs passed as the argument will be displayed in the same file.
Once the name has been constructed, the current value of $IO is saved for later use and the output device is switched to the named log file. The parameters used for the OPEN command ensure that the file will be created if it is not there. The timeout of zero indicates that Caché will try a single time to open the file and fail if it cannot.
Once the file has been opened, the code loops over the name value pairs. For each pair, the caller routine name and the entry point name are written followed in the line by the name-value pair. (If the value part is the empty string, only the name is written.) Each pair occupies one line in the log file. The first three values on each line are aligned so they appear in columns for easier scanning.
When all the pairs have been written, the log file is closed, the previous value $IO is restored and control returns to the caller.
^%ZSTART
This routine contains the entry point actually invoked by Caché. It uses the services of ^%ZSSUtil just described. All the entry points act more or less the same, they place some information in the log. The SYSTEM entry point has been made slightly more elaborate than the others. It places information in the Operator console log as well.
%ZSTART ; User startup routine. 
 
#Define ME "ZSTART"
#Define BgnSet "Start"
#Define Empty ""

    ; cannot be invoked directly
    quit
 
SYSTEM ;
    ; Cache starting
    new EntryPoint, Items
     
     set EntryPoint = "SYSTEM"
    
     ; record the fact we got started in the console log
     do WriteConsole^%ZSSUtil((EntryPoint
                               _ "^%"
                               _ $$$ME
                               _ " called @ "
                               _ $ZDATETIME($HOROLOG, 3)))
    
    ; log the data accumulate results
     set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3),
                           "Job", $JOB,
                           "Computer", $ZUTIL(110),
                           "Version", $ZVERSION,
                           "StdIO", $PRINCIPAL,
                           "Namespace", $ZUTIL(5),
                           "CurDirPath", $ZUTIL(12),
                           "CurNSPath", $ZUTIL(12, ""),
                           "CurDevName", $ZUTIL(67, 7, $JOB),
                           "JobType", $ZUTIL(67, 10, $JOB),
                           "JobStatus", $ZHEX($ZJOB),
                           "StackFrames", $STACK,
                           "AvailStorage", $STORAGE,
                           "UserName", $ZUTIL(67, 11, $JOB))
    do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
    
    quit
LOGIN ;
    ; a user logs into Cache
    new EntryPoint, Items
    
    set EntryPoint = "LOGIN"
     set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
    do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
    quit
    
JOB ;
    ; JOB'd process begins
    new EntryPoint, Items
    
     set EntryPoint = "JOB"
     set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
    do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
    quit
     
CALLIN ;
    ; a process enters via CALLIN interface
    new EntryPoint, Items
    
     set EntryPoint = "CALLIN"
     set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
    do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
    quit
Here is an description for each label:
^%ZSTART
This routine begins with a QUIT command so that it is benign if invoked as a routine rather than beginning its execution properly at one of its entry points.
This routine also defines named constants (as macros) for its own name, a starting string and the empty string.
SYSTEM^%ZSTART
This subroutine constructs a string consisting of the calling routine name, entry point, and the date and time it was invoked. Then it calls WriteConsole^%ZSSUtil to place it in the operator console log.
Afterward, it constructs a list of name-value pairs that it wishes to be displayed. It passes this to WriteLog^%ZSSUtil to place into the local log file. Then it returns to its caller.
LOGIN^%ZSTART, JOB^%ZSTART, and CALLIN^%ZSTART
These subroutines do not place any information in the operator console log. Instead, they construct a short list of items, enough to identify that they were invoked, and then use WriteLog^%ZSSUtil to record it.
^%ZSTOP
This routine contains the entry points actually invoked by Caché and it uses subroutines in ^%ZSSUtil. This example is similar to the example for ^%ZSTART. See the previous section for details.
%ZSTOP ; User shutdown routine. 
 
#Define ME "ZSTOP"
#Define EndSet "End"
#Define Empty ""

    ; cannot be invoked directly
    quit
 
SYSTEM ; Cache stopping
    new EntryPoint
    
    set EntryPoint = "SYSTEM"
    ; record termination in the console log
    do WriteConsole^%ZSSUtil((EntryPoint
        _ "^%"
        _ $$$ME
        _ " called @ "
        _ $ZDATETIME($HOROLOG, 3)))
    ; write the standard log information
    do Logit(EntryPoint, $$$ME)
    quit
    
LOGIN ; a user logs out of Cache
    new EntryPoint
    
    set EntryPoint = "LOGIN"
    do Logit(EntryPoint, $$$ME)
    quit
    
JOB ; JOB'd process exits. 
    new EntryPoint
    
    set EntryPoint = "JOB"
    do Logit(EntryPoint, $$$ME)
    quit
     
CALLIN ; process exits via CALLIN interface. 
    new EntryPoint
    
    set EntryPoint = "CALLIN"
    do Logit(EntryPoint, $$$ME)
    quit
    
Logit(entrypoint, caller) PRIVATE ;
    ; common logging for exits
    
    new items
    
    set items = $LISTBUILD($$$EndSet, $ZDATETIME($HOROLOG, 3))
    do WriteLog^%ZSSUtil(caller, entrypoint, items)
    quit
Extending Languages with ^%ZLANG Routines
Note:
It is customary (but inaccurate) to refer to a routine as if the caret is part of its name. This documentation follows this custom except when referring directly to the actual names of the routines.
You can use the ^%ZLANG feature to add custom commands, functions, and special variables to the ObjectScript and other languages. Your extensions are invoked in the same way as standard features, and follow the same rules for syntax, operator precedence, and so on. ^%ZLANG features generally do not execute as rapidly as standard Caché features. Consider this point when coding performance-critical routines.
To add such extensions:
  1. Define routines with the following names, as needed:
    %ZLANGCnn
    %ZLANGFnn
    %ZLANGVnn
    Routines with the name %ZLANGCnn define commands, routines with the name %ZLANGFnn define functions, and routines with the name %ZLANGVnn define special variables. The nn part of the routine name indicates the language in which these items are available. nn is one of the following:
  2. In these routines, define public subroutines as follows:
  3. As good programming practice, the first command in the parent routine should use QUIT so that nothing occurs if a user invokes the routine directly.
    It is also helpful for the routine to include a comment at the top that indicates the name of the routine itself.
Caution:
For other subroutines, including ones invoked by the public subroutines, make sure that the labels for those are in lowercase or mixed case (or do not start with Z). Or implement them as private subroutines.
That is, because a ^%ZLANG routine extends the language, it is important to make sure that only the desired subroutines are available outside of it.
Notes
Commands are handled like a DO of a routine or procedure. Arguments are passed as call parameters.
Your code should preserve the values of system state such as $TEST and $ZREFERENCE unless you intend for them to be a result of your code. (But note that for functions and special variables, the system automatically preserves $TEST.)
You can SET the value of a special variable. There is only one entry point for the variable. To determine whether to set the value or retrieve the value, your code should check whether an argument is given. For example:
ZVAR(NewValue) public {
         if $DATA(NewValue) Set ^||MyVar=NewValue Quit
         Quit $GET(^||MyVar)
}
Then, a user can either set this variable or retrieve it, as demonstrated here:
SAMPLES>w $ZVAR
 
SAMPLES>s $ZVAR="xyz"
 
SAMPLES>w $ZVAR
xyz
To return an error code from a command or function, use $SYSTEM.Process.ThrowError().
Examples
For example, to define a custom special variable for use in ObjectScript, you define the routine ^%ZLANGV00, which could look like the following:
 ; implementation of ^%ZLANGV00
 ; custom special variables for ObjectScript
  QUIT

ZVERNUM        ; tag becomes name of a special variable
ZVE
  QUIT $PIECE($ZVERSION,"(Build")


Then, for demonstration, you can use the new variable in the Terminal as follows:
SAMPLES>w $zvernum
Cache for Windows (x86-32) 2011.1
SAMPLES>w $zve
Cache for Windows (x86-32) 2011.1
For another example, suppose that you define the ^%ZLANGF00 routine as follows:
 ; implementation of ^%ZLANGF00
 ; custom functions for ObjectScript
 QUIT
 
ZCUBE(input) public {
 Quit input*input*input
}
Then, for demonstration, you can use the new function in the Terminal as follows:
SAMPLES>w $zcube(2)
8
The following example shows ^%ZLANGC00, which creates a command that executes the system status utility ^%SS:
 ; %ZLANGC00
 ; custom commands for ObjectScript
  QUIT    

ZSS       ; tag name of a command to check system status
  DO ^%SS
  QUIT