My Orders

Introduction

The My Orders window is used to browse previously submitted orders by specifying the desired date range. It illustrates the following Inq features:

The Window Menu Bar
Inq's support for setting up a window's menu bar and building menus.
Keys As Filters
The Order typedef defines a key called Filter. We look at how this key is applied to SQL and its use of Inq's caching system.
Listening For Created Orders In The Server
The server code for My Orders listens for events raised when new orders are created and uses them to maintain the summary table of orders.
Listening For Node Events In The Client
Using event listeners in the client is just as valid as the server. My Orders uses a listener to keep the items totals row up to date when an order is selected in the summary table.
Writing Reports
My Orders implements pdf reports by using Inq's ability to transform node structures into XML and deploys Inq's file transfer package to return the generated files to the client.
Table GUI Events
Any items in an order that are not yet shipped by the Order Processor can be cancelled by clicking on the icon in the status column of the Items table. Code for this table is shared with the New Order function - soliciting table events and the information Inq provides in them is described there.

Here is what the My Orders window looks like:

myorders

The Menu Bar

To add a menu bar to a window, set its menuBar property:

// Menu Bar
myOrders.properties.menuBar = call createMenu(context=myOrders, i18n);

The menu bar and contents are created by the createMenu() function. My Orders just has a File menu:

gMenuBar menuBar;

gMenu fileMenu;
fileMenu.properties.text={i18n}.ps.button.FILE;

Next, the menu items are created and initialised, as in this example (the others have been omitted for brevity):

// Generate a printable of the filtered orders
gMenuButton filePrintSummary;
filePrintSummary.properties.text={i18n}.ps.button.PRINT_SUMMARY;
filePrintSummary.properties.icon = $catalog.icons.pdf16;
gEvent(filePrintSummary, call printSummary());

The following statement lays out the menu and its menu items:

// Layout the menu
layout(., context, menuBar,
       "fileMenu
        {
          fileNew
          Separator
          filePrintSummary
          filePrintOrder
          Separator
          fileClose
        }
        ");
Note
In this example of layout() there are four arguments, because the graphical root is not the same as the Inq node.

The graphical root is menuBar while the Inq node where the components are all placed is context (the myOrders window). The menuBar component accepts menus as its immediate children and the open-brace { causes layout to descend in the graphical hierarchy. This menu includes separators, created during the layout with the Separator keyword. Here is the result:

myordersmenu

Menus can be nested in the layout string by using successive matched braces to define the menu items at the next level.

Using a Filter

The Order typedef defines a key called Filter. The purpose of this key is to return a lesser or greater number of instances according to tighter or looser input values. To wild-card an input value the key implementation uses Inq's null value. Furthermore this key uses two input values (of type date in this example) to specify a range.

Key Definition

Here is the implementation of the key from Order.inq:

key Filter cached=true
(
  // This example defines fields in the key that are not part of the
  // typedef. FromDate and ToDate exist in the key only.
  fields(Account,
         Status,
         Date.Date FromDate,
         Date.Date ToDate)

  eligible
  (
    // This key is cached while at the same time containing an inequality.
    // Once an order is placed the OrderDate field never changes but we
    // could just as well create new Orders whose OrderDate would satisfy
    // a cached key's date range. To maintain a correct cache we define an
    // eligibility expression. It must match the sql where clause.
    $this.instance.Account   == $this.key.Account &&
    ($this.instance.Status   == $this.key.Status || isnull($this.key.Status)) &&
    ($this.instance.OrderDate >= $this.key.FromDate || isnull($this.key.FromDate)) &&
    ($this.instance.OrderDate <= $this.key.ToDate || isnull($this.key.ToDate))
  )

  // When defining the database binding information for this key
  // we include a "read-order" element. This is because the key
  // fields are used more than once in the where clause, that is
  // we cannot rely on the key field declaration order to initialise
  // the prepared statement parameters.
  auxcfg( map(
  "prepared", true,
  "read-sql",
    "
      {select-stmt}
      where O.userid = ?
      and (O.Status = ? OR ? IS NULL)
      and (O.orderdate >= ? OR ? IS NULL)
      and (O.orderdate <= ? OR ? IS NULL)
    ",
  "read-order",
    array a = (
                "Account",
                "Status",       "Status",
                "FromDate",     "FromDate",
                "ToDate",       "ToDate"
              )

  ))
)

This key defines four fields. We can see in its SQL where clause and associated configuration how these are used, including an array to guide the parameter substitution.

Keys are discussed in More About I/O Keys. The eligible() expression mirrors the where clause, so when new orders are created or the Status field of existing ones changes Inq maintains its cache accordingly.

The Client

The filter components are created and laid out as a unit in gui/myOrders.inq:createFilterBar().

myordersfilter

The data these components are bound to is an instance of the Order.Filter key:

// Create the data the filter bar components will operate on.
// This is an instance of Order.Filter
any context.vars.filter = new(Order.Filter);

The components themselves are created much like other the other examples already discussed however there are a few interesting features used by the filter.

Check Box Values

We saw when looking at the popup menu used in the Item Chooser how check components support different values for their checked and unchecked state. To implement the desired functionality of the filter's check box we can set up these values as follows:

gCheck cStatus;
cStatus.properties.checkedValue = enum(OStatus, O);
cStatus.properties.uncheckedValue = null;
cStatus.properties.renderInfo = renderinfo($this.vars.filter.Status, label={i18n}.ps.button.OPEN_ONLY);

With these properties in effect, toggling the check box sets the Status field of the filter to the OPEN order status or null, as we require.

The Date Choosers

Inq incorporates Kai Toedter's JCalendar as its date chooser component. For the filter we use two date choosers like this:

// Two date choosers for the date range
gDateChooser dcFromDate;
gDateChooser dcToDate;
dcFromDate.properties.renderInfo = renderinfo($this.vars.filter.FromDate,
                                              typedef=Order.OrderDate,
                                              format="dd MMM yyyy",
                                              width=8,
                                              label={i18n}.ps.general.FROM_DATE);
dcToDate.properties.renderInfo   = renderinfo($this.vars.filter.ToDate,
                                              typedef=Order.OrderDate,
                                              format="dd MMM yyyy",
                                              width=8,
                                              label={i18n}.ps.general.TO_DATE);

// The from date cannot be null (which referring to Order.Filter
// means the field is ignored) so we are preventing the user from going
// back to the beginning of time.
dcFromDate.properties.nullText   = null;

By default a date chooser allows the null value. This can be established by the user hitting ctrl-n in the focused date chooser. If the component is rendering the null date it displays the default text <none>. The nullText property allows this text to be changed or the null value to be prevented by setting nullText to null.

Resetting The Filter

Included in the filter components is a reset button. This is scripted to call the function resetFilter() function, which demonstrates date arithmetic:

local function resetFilter(any filter)
{
  // Default the from date to today minus one month. Default
  // the end date to open
  filter.FromDate = dateadd(MONTH, -1, getdate());
  filter.ToDate   = null;
}

The createFilterBar() function returns the component it laid out its components in, so that client script can place it in the wider GUI under construction.

The Server

The filter is used in two ways by server-side script, found in psOrders.inq.

Applying The Filter

The client invokes the filterOrders service, supplying the filter as set by the GUI and the path where the server should place the resulting node-set.

filterorders

This service makes no formal response. Instead it uses event propagation from server to client as the means to return the data. Inq's automatic handling of such events received at the client is to place the node set at the same path, in turn generating an event in the client which is dispatched to the Orders table.

Listening For New Orders

When discussing the New Order function we showed how the event raised by Inq on successful instance creation can carry some user-supplied data. This data acts as additional discrimination, allowing consumers to filter events of interest to them.

In this example an instance of Order.Filter has been used for this data. Using a key instance is just a convenience - any kind of map, however created, can be used. When considering how parts of the system communicate via events, filtering them is part of the design. For a given event flow (in this case Order creation) all participants must agree on the form the create event data will take.

Our event flow arranges for new orders to appear on the My Orders screen, provided the order matches the event filtering currently in place. The following diagram depicts this flow:

listenorders

Although this diagram shows two clients cooperating via their presence in the server, its equally valid (and more appropriate in petstore) that a User Process receives its own events. Inq makes no distinction in this regard.

The filterOrders() service arranges to receive Order creation events with the following statement:

any $this.listeners.newOrder = listen (unlisten($catalog, $this.listeners.newOrder),
                                       func f = call newOrderCreated(listenInfo, Order = @eventData),
                                       event = (create),
                                       create  = call makeCreateListenData(filter),
                                       typedef = Order,
                                       listenInfo);

In fact, there are two complementary Inq functions being used - listen() and unlisten(). The arguments to listen() are as follows (the first three arguments are mandatory, the remainder optional):

listen() example arguments [Full explanation of listen() is TODO]
Name Description

The node to listen to. When listening for create events this is always $catalog because all such events emanate from there.

Note that unlisten (explained below) returns its first argument.

The function to dispatch to. Does not have to be a call statement, instead it can be any closure style statement.

When the function executes the stack is initialised by Inq as explained below.

event = (...)

A comma-separated list of event types. This example is only interested in create events.

create = <expression>

The create listen data. This is the complement of the value produced by makeCreateRaiseData() discussed in New Order.

typedef = <typedef-literal>

A literal type reference whose create events we want to receive.

listenInfo

An example of a trailing argument whose value is placed on the stack when an event is dispatched.

The return value of listen is a token that represents the event binding. Script can retain this token for future use in removing the binding with unlisten().

Note
Other than when replacing a listener with a new one, it is not necessary to tidy listeners up. If the User Process terminates Inq will remove defunct listeners automatically.

The unlisten() function itself returns its first argument (the node from which the event binding is being removed) and tolerates that its second (the event binding) is unresolved, to support the listen(unlisten(...)) construct. Each time filterOrders() is invoked any existing listener is discarded and a new one established with the prevailing filter data represented in the create argument, returned by the call to makeCreateListenData(any filter):

local function makeCreateListenData(any filter)
{
  any createData.Account   = isnull(filter.Account, equals);
  any createData.Status    = isnull(filter.Status, equals);
  
  // Event filtering is only by equality. As we are dealing with a date range
  // we let all creations through and check in the dispatch function. This is
  // OK because in this case we are only listening to our own data.
  any createData.FromDate  = equals;
  any createData.ToDate    = equals;

  // returns
  createData;
}

We can see that this function creates its map "by implication" rather than using an instance of Order.Filter like makeCreateRaiseData() does. This is incidental. What is important is that the map content (which must use the same key set as the one used in create()) wild-cards any of its fields using the equals constant. This is a special value that always yields true when compared with anything.

You may notice that the FromDate and ToDate are always set to equals. This is because at present, discrimination by event data only supports equality. Given that these parameters specify a range, there is currently no way to express this. Instead the dispatch function performs an additional check to see that the new Order's Order.OrderDate falls within the the current filter's range:

local function newOrderCreated(any Order, any listenInfo)
{

  if (call isNewInstanceInDateRange(listenInfo.filter, Order))
  {
    // Make a node set child for the new Order
    hmap m;
    any m.Order = Order;

    // Add it into the list, causing it to appear on the GUI screen.
    any pKey = getprimarykey(Order);
    add(m, path($this.{listenInfo.at}.{pKey}));
  }
}

What processing does the dispatch function perform? It needs to add the new order to the current node-set structure in the server:

  • it creates the map m as a suitable node-set child;
  • places the new Order beneath m as m.Order;
  • acquires the unique key of the Order and uses it to add m to the node-set.

The last step causes the new node-set child to be propagated to the client where, similarly when applying the filter, it is replicated at the same path, processed by Inq's MVC and dispatched to the table.

Structuring The Code For Desktop and Web Clients

Long before reaching this point you will have realised that the Inq implementation of petstore is not a web application as such. Of course, this refers only to the client, which has been implemented to demonstrate Inq's capabilities with respect to the desktop.

As well as this Inq supports the construction of web-based applications, allowing the server-side script to be reused but changing the client technology. What are the differences between the two architectures?

Comparison Of Desktop and Web-based Inq Applications
Desktop Web
Client has a permanent, bidirectional connection to the server Client makes a connection to the server for each request
Server may invoke services in the client or send it events Server cannot initiate an exchange with the client
Server maintains complex state on behalf of the client Server maintains minimal state for subsequent client connections
Server knows when client disconnects Server times out client if it never connects again
Client/Server custom protocol Client/Server standard protocol (including firewalling)
deskweb.png
Note
Explanation of how Inq generates JSON when working with web apps and integration with JavaScript is TODO.

For filtering orders, the call structure for the desktop is as follows:

Client
 |
 |--service filterOrders()
     |
     |-------call filterOrders()
               |
 |<--event-- add list to node space
             establish new order listener
        

Adding the current orders list to the node space and listening for new orders are things only relevant to the desktop client. In the web case, the return value of the service makes up the data returned to the browser client. The process of applying the filter is factored out into the function filterOrders, and a suitable entry point webFilterOrders is created for it. The call structure for the web app case looks like this:

Browser
 |
 |--service webFilterOrders()
     |
     |-------call filterOrders()
               |
 |<--returns---| 
        

In the web app case there is no persistent state held in the server, other than the session's connection to the Inq server.

Listening For Events in the Client

In the majority of cases, the listsners Inq sets up internally when binding views to models are sufficient to drive the GUI from the events occurring in the node space. Sometimes however, it is necessary to script explicit event handlers for node events.

My Orders needs to update the total order value, displayed in the items table totals row, when a selection is made from the filtered orders list. The listen() function is equally supported in the client as it is in the server; the client sets up a listener with this statement:

// listen to $this.itemTable.properties.modelRoot so that when
// the items list is replaced the order total is displayed
listen ($this,
        func f = 
        {
          call calcOrderTotal();
        },
        event  = (add, remove, replace),
        path   = $this.vars.items  // NB listen path must be literal at the moment
       );

This example listens to the context node, $this, and uses listen()'s optional path argument to discriminate for events originating at the root of the items data, $this.vars.items. If a node is added at that location, removed or replaced with a new one (seen as distinct from add) the listener will fire. The function calcOrderTotal (in gui/newOrder.inq) simply reevaluates the rendered data:

function calcOrderTotal()
{
  $this.vars.orderTotal.row.OrderTotal.Price =
    $this.vars.items ? isnull(sum($this.vars.items, $loop.LineItem.Qty * $loop.LineItem.UnitPrice), 0)
                     : 0;
}

Creating Printable Reports

My Orders includes two printable reports - one at a summary level for the filtered order set and one for a selected order and its items. Writing reports in Inq combines Inq's ability to transform a node structure into XML with an external PDF generator such as Apache FOP. This XML is processed against an XSLT style sheet to create the report as <fo:... Formatting Objects XML.

fopreport

Report generation is performed by the server and petstore deploys the Inq bundled package filexfer to return the PDF result to the client for display or print. In this way Inq maintains its minimal software requirements of the desktop environment to just a JavaTM runtime environment and any suitable PDF reader associated with that file type.

The XML Report Content

The XSL templates used to transform the XML report content into Formatting Objects can be found at reports/psreports.xsl, which references the subordinate files pagelayout.xsl and tableutils.xsl. These templates have been written as examples and infer a particular layout of XML document they can process. You may have existing templates that require a different format of XML input document. In that case you will need to modify the Inq script that generates this file, so bear in mind that the explanation of petstore's reports includes a XML/XSL design that we do not dwell on here.

Further discussion uses these examples of the order summary and order detail (pdf).

The XML that generates these reports is respectively ordersummary.xml and orderdetail.xml. Here is a partially expanded view of ordersummary.xml:

ordersummary1

The file contains four sections:

<report>
Fixed information such as the report title and the footer.
<summarytable>
A tag to define the table caption, its columns and where in the data section the row data for this table should be extracted.
<data>
All the data for the report. For the Order Summary report this is the Account and the list of orders including the totals row.
<INQmetadata>
Meta-data about all the typedefs instances of which are present in the <data> section. Much like the Inq client GUI does, this information may be used by the templates to generate row headers, dimension columns and so forth.

The <data> and <INQmetadata> sections are known as reference sections whereas <report> and <summarytable> are called content sections.

How The Templates Work

Briefly, the templates generate their Formatting Objects output by processing the content sections against the <data> section to provide the values and the <INQmetadata> section, where widths, labels and enumerations are held.

Using attributes common to both, the templates use XPath expressions to extract information from the reference sections as directed by a particular element of the content.

The ioXMLX Stream

Inq includes a stream type called ioXMLX which writes Inq structures to its sink as XML or parses XML from its source into an Inq structure.

Note
In Inq a particular stream type defines the format that data is read or written in (for example XML, plain text or some other format) and its connected source or sink the physical medium where it is read or written. Sources and sinks are specified with a URL syntax, such as file://<path> or socket://<host:port>

Conversion of an Inq structure to XML involves the following steps:

  1. Declare a stream of type ioXMLX
  2. Configure the stream using the properties it supports
  3. Open the stream for writing, binding it to a sink
  4. Write one (or more) values to the stream, closing it when done.

Comparing Inq and DOM Structures

Before looking at this process in more detail, it is useful to compare Inq and Document Object Model structures to see how they differ. An Inq structure is a simple containment hierarchy in which nodes are either maps, which may contain children, or leaves of some data type. In a map, the key set must be unique. Map children may or may not have an ordinal position.

In a DOM structure there are various node types including elements, attributes and text. Elements may contain other elements, attributes or text. Elements have a name and child elements do not have to be uniquely named in their parent. Text nodes are unnamed but their ordinal position, alongside that of their sibling elements, is significant.

The construction of a suitable Inq structure and use of an appropriately set up ioXMLX stream support the production of elements, attributes and text as either XML text, in-memory as a tree rooted at an instance of org.w3c.dom.Document or as JSON text.

Configuring ioXMLX

The ioXMLX stream supports the following properties used to configure its behaviour:

ioXMLX Properties
Name Description Default
cdata A set of path()s that will be produced as CDATA sections. Otherwise numeric character references or character entity references are produced for markup and special characters.
childName When writing node sets, the name given to the element representing a node-set child. child
DOMOutput Generate output as an in-memory org.w3c.dom.Document. Must be set before the stream is opened false
enumExt When true produce any enumerated value fields as their external representation. false
excludesAt A set of path()s that will be excluded (and therefore their children excluded) from the production.
excludesBelow A set of path()s that will themselves be produced but their children excluded from the production.
formatOutput When producing XML or JSON text, whether the output will be formatted as indented lines for readability. true
formatters A map whose keys are path()s and values are formatters (or format patterns). When the traversal produces a scalar (generated as a text node) and the current path compares equals [TODO link] with a map key the corresponding format is applied.
groupingUsed When true grouping separators are produced in numeric text; when false they are not. Of particular relevance to JSON output. true
includes As a complement to the excludes[At,Below] properties, a set of those paths that will be produced in the output; everything else will not be produced.
inqAttributes When node sets, typedef instances and their fields are encountered, whether the production of these elements includes attributes that identify the element as such. See also writeMeta below.
JSONOutput Generate output as JSON text. Must be set before the stream is opened false
metaFunc A function, which must be a call() statement, that will be called as the meta-data section of the output is produced. See following section. Can be used to manipulate the production of the meta-data, such as changing widths or labels from their typedef defaults. If set then also sets writeMeta to true
metaName The name of the root element for the meta-data section of the output. If set then also sets writeMeta to true INQmetadata
preserveTypes When generating tags for value types whether type information is included in the output so that the same types are restored on input. If false then strings will be created. false
rootName The name of the root element of the produced document. root
seed A map used to seed the structure created when parsing XML. smap
tagFuncs A map whose keys are path()s and values are functions, which must be call() statements. As the traversal proceeds the path is maintained. If the current path compares equals [TODO link] with a map key the corresponding tag function is called. Used to manipulate the name of the element produced, apply attributes to it and manipulate any text content.
tagNames A map whose keys are path()s and values are strings. If the current path compares equals with a map key the corresponding value is used as this element's name. Can be used instead of a tagFunc if the only requirement is a fixed element name change.
writeMeta When true the meta-data section will be written to the output; when false it will not. If set to true then implies inqAttributes is also true and vice-versa. false
writeMultiple When true successive writes to the stream are enclosed in a <write> element; when false the stream is intended to produce its document with a single write operation on the root of the Inq structure. false
xmlPrologue When producing XML text (the default output format), the prologue to use. The default file encoding is included in the default value. <?xml version="1.0" encoding="default-platform-encoding"?>
xmlOutput Generate output as XML text. true

By default, ioXMLX will generate an XML text document for a single Inq node structure, converting maps into elements containing child elements and leaves into elements with child text. By setting writeMeta to true the <INQmetadata> section is created automatically. Referring back to ordersummary.xml, for the other sections the remaining issues are

  1. generation of elements with repeated names, such as <column> - Inq does not allow duplicate names at a given level;
  2. production of any required attributes - Inq does not have any concept of different types of children, such as attributes and elements, as DOM does;
  3. the output is correctly ordered, for example the <column> elements must appear in order for the desired result in the PDF table and the rows (in the <data> section) appear by ascending order number, with the total row last.

These things can be achieved by building an appropriate Inq structure, sorted as necessary, and configuring ioXMLX.

Focus on the Order Summary Report

Creating the Inq Structure

Referring to psReports.inq the ordersummary.xml file is created by the function createSummaryXML(). It is passed the current filter from the GUI which it uses to re-fetch the current order set, in this case using an ordered map (an omap) to seed the structure:

local function createSummaryXML(any filter)
{
  omap m;
  any root.data.orders  = call filterOrders(filter, root = m);

Notice that the node root.data.orders has been created - the node data is the beginnings of what will become the <data> tag in the XML production. Next, the total row for the report is created by making a new instance of Order, setting its TotalPrice field to the sum of the orders and placing it in the structure alongside them:

  any total = new(Order);
  total.TotalPrice = sum(root.data.orders, $loop.Order.TotalPrice);
  any root.data.orders.total.Order = total;

The orders node is an omap, so it supports vector access and therefore sorting. The table rows appear in ascending order of date and order number, according to the outcome of this use of sort:

  sort(root.data.orders, $loop.Order.OrderDate, $loop.Order.Order, null=NULL_HIGH);

The Order at root.data.orders.total.Order has uninitialised fields Order.Order and Order.OrderDate. These fields have no default value set for them in the typedef, so they have the value null. By telling sort() that null sorts above non-null values with the optional argument null=NULL_HIGH the total row will be at the end of the vector.

The user's Account is required for the report, so this is added to the <data> section. The <report> tag contains the title and disclaimer:

  any root.data.Account        = call getAccount();
  any root.report.title        = "Order Summary"; // TODO i18n
  any root.report.disclaimer   = call disclaimer();

The Inq function renderf formats a string as for MessageFormat with the additional semantics that if typedef fields are referenced their format pattern is used. To make use of this petstore defines the typedef ReportHelper (see ReportHelper.inq). To create the table caption an instance of ReportHelper is initialised from the filter's From and To dates. The rendered text is then placed in the omap root.summarytable:

  any helper = new(ReportHelper, filter);

  omap root.summarytable;
  
  any root.summarytable.caption = renderf("Your orders between {0} and {1}:",
                                          helper.FromDate,
                                          isnull(helper.ToDate, "Today"));

An omap is used so that as columns are added their order is preserved. According to the requirements of an Inq map's unique key set, the columns are set up like this:

  any root.summarytable.OrderNumber = null;
  any root.summarytable.OrderDate   = null;
  any root.summarytable.OrderTotal  = null;

The values are not important, in fact the column tags in the XML are empty. The names, OrderNumber, OrderDate and OrderTotal are picked up by a tag function so that a <column> tag with the required attributes can be produced. Furthermore, data for the table rows must be retrieved from somewhere so we need to generate the <from> tag:

  any root.summarytable.from        = null;

Using ioXMLX

Having built an Inq structure that looks somewhat like the XML document we want we are ready to apply it to ioXMLX. The stream is declared and the tagFuncs property set so script can intervene during the traversal to manipulate the output:

  // Now write the xml out. Declare a stream
  ioXMLX strm;

  object p = path($this.summarytable.%);
  any tagFuncs.{p} = cfunc f = call orderSummaryChild(nodeset = getnodeset(root.data.orders));
  strm.properties.tagFuncs = tagFuncs;

As stated above, this property is a map of paths to call statement functions. In order to get a path to be a map key it must be protected from expansion as an indirect path by placing it inside an object variable.

Note
In general, variables of type object allow foreign objects (like a DOM tree for example) to be carried around in the Inq environment.

When is the tag function called and what does it do? As ioXMLX traverses an Inq structure it maintains the path of the current node. If this path matches a key in the current tagFuncs then that function is called before the corresponding element is produced. Here is the signature of orderSummaryChild():

local function orderSummaryChild(any nodeset,   // <- argument(s) from script
                                 any node,      // <- arguments Inq supplies
                                 any parent,    // v
                                 any nodeName,
                                 any ordinal,
                                 any content,
                                 any last,
                                 any descend,
                                 any attributes)

The nodeset argument is explicitly provided in the call to the function. The other arguments are provided by ioXMLX and have these meanings:

Tag Function Arguments
Name Description
node The current Inq node
parent The parent of the current Inq node
nodeName The name of the current node (that is its key in its parent)
ordinal The zero-based ordinal position of this node in its parent. If the parent does not support vector access then this argument is still supplied but does not infer order. It should be considered as a simple counter.
content If custom content is to be produced the tag function may return it by setting the value of this argument.
last true if this is the last child of the current parent, false if it is not.
descend Initialised to true before the function is called, may be set to false to prevent the current node being descended into.
attributes A map that the function may populate with string key/value pairs that will be produced as XML attributes on the produced element.

The return value of a tag function is the desired element name. If this is nodeName then that argument should be returned. If null is returned then the current element is not produced in the output (and not descended into).

Here is the body of orderSummaryChild():

  any ret = "column";
  
  switch
  {
    when(nodeName == "from")
    {
      any attributes.nodeset = nodeset;
      
      // Leave the tag name as it is
      any ret = nodeName;
    }

    when(nodeName == "caption")
    {
      // Leave the tag name as it is
      any ret = nodeName;
    }
    
    when(nodeName == "OrderNumber")
    {
      any attributes.typedef = fqname(typedef(Order));
      any attributes.field   = "Order";
      any attributes.halign  = "center";  // used by table cell generation
    }

    when(nodeName == "OrderDate")
    {
      any attributes.typedef = fqname(typedef(Order));
      any attributes.field   = "OrderDate";
    }

    when(nodeName == "OrderTotal")
    {
      any attributes.typedef = fqname(typedef(Order));
      any attributes.field   = "TotalPrice";
    }
    
    // Don't produce a tag for anything we were not expecting
    otherwise
      any ret = null;

  }
  
  ret;

and referring to the sample XML output above we can see how this script plays its part in transforming the original Inq node structure into the desired result.

As noted above, the chosen XML production relates to the processing by xlst templates into Formatting Objects. Out of interest, how does this work? Considering the orders table defined by <summarytable>, the templates find the root of the rows data by tieing together the @nodeset attribute of the <from> tag with the element underneath <data> whose @nodeset attribute has the same value.

ordersummary2

Similarly for the column data, the @typedef and @field attributes are matched up as shown.

The total row has a currency symbol in its formatted result. To achieve this we can override the default formatting (specified in its typedef) of the Order.TotalPrice field. The formatters property is set up in the same way as tagFuncs was, mapping the path whose format is to be established to a format pattern:

  object p = path($this*total.Order.TotalPrice);
  any formatters.{p} = "¤#,##0.00";
  strm.properties.formatters = formatters;

  call writeXMLFile(strm, root, rootElementName = "ordersummary");

Having set up those properties specific to the order summary report, the remaining work to generate the XML file is carried out in the helper function writeXMLFile()

local function writeXMLFile(any strm,
                            any root,
                            any rootElementName)
{
  any repXml = createtmpfile($process.loginName, ".xml");
  try
  {
    strm.properties.rootName = rootElementName;

    strm.properties.metaFunc = cfunc f = call metaTags(priceCurrency = $catalog.{$root.i18n}.ps.CURRENCY);
    
    open(strm, repXml, OPEN_WRITE);
    writestream(strm, root);
  }
  finally
  {
    close(strm);
    
    // returns
    // Note: the Inq "file" type yields a URL syntax when converted to a string.
    // We want the file system's path instead. 
    repXml.properties.canonicalPath;
  }
}

In this function the stream's metaFunc property is set. Setting this property implies also that the <INQmetadata> section will be produced. Here is a sample from the order summary production:

ordersummary3

As ioXMLX encounters typedef instances during the traversal so it writes out their metadata as shown. Within each <INQfield> tag at most the following can appear:

Tag Description
<INQisnumeric> Present if the field is a numeric value and has the text content true. Absent otherwise
<INQwidth> The width of the field
<INQlabel> The label of the field
<INQenum> Any enumerations the field has. The internal value is given by the attribute @INQinternal and the external value is the content. Repeated for each enumerated value of the field.

The <INQtypedef> and <INQfield> tags respectively have the @INQfqname and @INQname attributes that allow templates to make use of the metadata. The sample templates do this to generate the table headers, dimension the columns and right-justify numeric values.

A metaFunc is called once for each field within each typedef and allows script to control what gets produced in the <INQmetadata> section, so that if the typedef defaults are not suitable alternatives can be provided. Here is what our example does:

local function metaTags(any priceCurrency,  // <- argument(s) from script
                        any Typedef,         // <- arguments Inq supplies
                        any Field,           // v
                        any Label,
                        any Width)
{
  switch
  {
    when (Typedef == typedef(Order) && Field == "ShipCity")
    {
      Label = "Shipping\nCity";
    }
    
    when (Typedef == typedef(LineItem) && Field == "UnitPrice")
    {
      Label = "Unit Price\n" + priceCurrency.properties.symbol;
    }
    
    when(Typedef == typedef(ValueHelper) && Field == "Price")
    {
      Label = "Total Price\n" + priceCurrency.properties.symbol;
    }
  }
}

The priceCurrency argument is explicitly provided in the call to the function. The other arguments are provided by ioXMLX and have these meanings:

Tag Function Arguments
Name Description
Typedef The typedef whose metadata is being produced
Field The field whose metadata is being produced
Label The default label. The function can assign an alternative value to this argument.
Width The default width. The function can assign an alternative value to this argument.

We can see that some columns have a new-line in them. It is also possible to access a currency symbol (via property access and Inq's currency data type) and include this in the header. [Note: orderdetail.pdf uses these].

Lastly, the stream is opened to a temporary file and the XML created with the call of writestream(strm, root);.

Generating the PDF Output

Having discussed createSummaryXML(), the order summary pdf is created by the following function:

local function orderSummary(any filter, any ack)
{
  // Create the xml report file.
  any xml = call createSummaryXML(filter);
  
  // Generate the pdf report
  any pdf = call generatePdf(xml); 

  // Return the generated file name to client. It then performs
  // a file transfer request  
  send updateOk(item = pdf, ack);
}

The local variable xml is the path of the file in which the XML production of the report has been created. What does generatePdf() do with this? Here is what that function looks like:

local function generatePdf(any xml)
{
  // xsl scripted report transform
  any xsl = call makeXslFile();
  
  // Where the result will end up
  any pdf = call makeOutputFile();
                      
  any c = "fop -xml {0} -xsl {1} -pdf {2}";
  string cmd = renderf(c, xml, xsl, pdf);

  // Put stderr in a string stream and if fop's exit status is not
  // zero throw the string content.
  string err;
  ioPrint perr;
  open(perr, "string://err", OPEN_WRITE);
  
  // syscmd closes any streams it is given so we don't have to
  if (syscmd(cmd, stdout=$catalog.system.out, stderr=perr) && err)
    throw(cmd + err);

  // Returns
  call inq.filexfer:makeRelativeToRoot(filename = pdf);
}

In fact, this is more a demonstration of Inq interfacing with the command line on the host. The important points are these:

  1. Build the command line to run Apache FOP using the renderf function described above.
  2. Create a ioPrint stream and open it to write to a string variable.
  3. Run the command using Inq's syscmd function, capturing anything it writes to stderr and throwing an exception should there be any and fop has a non-zero exit status.

For completeness and the sake of showing additional Inq features in use, here are the subordinate functions generatePdf() calls:

local function makeXslFile()
{
  // Expecting to find a directory ./reports relative to this inq source
  // file, where the reports are scripted as xslt.
  file xsl = absurl("reports" +
                    $properties.file_separator +
                    "psreports.xsl");
                      
  xsl.properties.canonicalPath;
}
  1. The absurl() function converts a relative path into an absolute URL string using either a specified absolute URL as the base as its second argument or (as here) the URL of the executing script is assumed.
  2. A variable of type file is assigned from the absolute URL. This is just so that a path of the form /some/absolute/path/psreports.xsl can be retrieved using the file's canonicalPath property. The fop command requires file paths, not file: URLs.
local function makeOutputFile()
{
  // The three-argument version of createtmpfile takes prefix, suffix
  // and directory. We are expecting to find <filexfer>/tmp as the
  // output directory.
  any f = createtmpfile($process.loginName,
                        ".pdf",
                        call inq.filexfer:makeAbsoluteFromRoot(filename = "tmp"));

  f.properties.canonicalPath;
}
  1. The createtmpfile() function returns a variable of type file, generating a name from the prefix, an optional suffix and in an optional directory. If no directory is specified the host's default temporary directory is used.
  2. The Inq bundled inq.filexfer package is discussed elsewhere [TODO]

The Order Detail report uses the same techniques and helper functions as Order Summary. There is only one thing it does differently, just for the sake of pointing it out. In createDetailXML() the formatter for the Total Price cell of the order (not the items) table is created like this:

object p3 = path($this.data.Order.TotalPrice);
any priceFormatter = format("¤#,##0.00", Order.TotalPrice);
any formatters.{p3} = priceFormatter;
strm.properties.formatters = formatters;

The format() function creates a formatter, in this case saving ioXMLX the trouble of doing so. It requires a variable whose type (numeric or date) is suitable for the pattern specified. Property support in formatters allows things like grouping separators to be disabled, negative prefix and suffix access and more [TODO].

nextpage