![]() |
|
|
Alexandre Fayolle Olivier Cayrol |
Table of Contents
This document is aimed at programmers who want to code modules for Narval. The preference language for coding modules is Python, and some knowledge of the python programming language is assumed. Having a previous experience in python programming and a good acquaintance with the python standard library definitely helps. Narval represents the data it manipulates as trees, using the XML syntax and the Document Object Model (DOM) interface. A short introduction to XML and DOM can be found in Appendix A..
Narval is a powerful system. Yet, left alone it is as dumb as a computer with only a bare bone operating system. Modules and actions provide means for Narval to interact with the Outside World Where It's Cold. Actions can be very simple or very complex, they can do the processing themselves or act as interfaces to outside programs, enabling different proprietary pieces of software to talk to each other.
Why should you write modules? Well, basically, there are two families of reasons. The first one is that you need to do something with Narval, and nothing exists yet to do it. This situation is likely to become less frequent as time passes, but since Narval is still young it may be the case. So you have checked the repositories, asked a few questions on the mailing lists, and it looks like you have found something missing. Well in that case, it is time to sharpen your Swiss army coding knife and confront yourself with writing the missing action. This book is here to help you, and so are the various mailing lists about Narval.
The second one is that you may have written a new program, and you think that the Narval system is soooooo cool that you just have to write a module to interface your program with Narval, so that other users may pilot your program through recipes. This book is here to help you in that case too.
In both cases, we thank you for contributing your efforts to the Narval user community.
The action is the fundamental brick used by Narval to perform tasks. It is composed of two parts: an XML prototype and a python stub, both of which appear in the same file. An action is usually included in a Module (see Chapter 1., Writing an action).
<!ELEMENT action (description*|input*|output*)> <!ATTLIST action name CDATA #REQUIRED> <!ATTLIST action func CDATA #REQUIRED> <!ELEMENT description (#PCDATA)> <!ATTLIST description lang CDATA #REQUIRED> <!ELEMENT input (match*)> <!ATTLIST input optional (yes|no) "no"> <!ATTLIST input use (yes|no) "no"> <!ATTLIST input list (yes|no) "no"> <!ATTLIST input outdates (yes|no) "no"> <!ATTLIST input id CDATA #IMPLIED> <!ELEMENT output (match*)> <!ATTLIST output optional (yes|no) "no"> <!ATTLIST output id CDATA #IMPLIED> <!ELEMENT match (#PCDATA)>
(1) | the action element is used as a container for the action prototype. It holds a number of input and output child nodes. |
(2) | name is the name of the action. It is used to identify an action in a recipe. To avoid name clashes, the name of the module is prepended and used as a namespace, so the name of an action needs only to be unique within a given module. |
(3) | func is the name of the python function that implements the action stub. This function must to be in the same module. The name is often the name of the action suffixed with _f. |
(4) | description elements are used to provide useful information about the action. The lang attribute is used to specify the language of the description. It is useful for graphical interfaces which can use this for localization |
(5) | the input element describes one input of the action. This description is a list of match elements each of which contain an XPath: in order to be accepted for the input, an element must match all these XPaths. |
(6) | if optional is set to yes, then the lack of element matching the input or the output will not cause an error. |
(7) | if use is set to yes this means that once an element has been passed as an input to the action, it will be flagged as used and not be reused by the action in subsequent evaluations of a plan instantiated from the same recipe |
(8) | if list is set to yes, several arguments matching the input can be passed to the action. This implies that the function stub is coded accordingly. |
(11) | the output element describes one output of the action. This description is a list of match elements each of which contain an XPath. Narval considers that an action has failed if each output is not matched exactly once by one different element output by the action (with the possible exception of optional outputs. all these XPaths. |
(9) | if outdates is set to yes, once an element has been passed as an input to the action, it will be flagged as outdated, and not be reused by any action or transition in Narval. You can think of it as a super use attribute, that affects all the recipes, and not just the recipe that instanciated the current plan. |
(10) | The id attribute is used to provide a way for steps to alter the prototype of the action, by modifying the attributes of the input, or by adding match nodes to the prototype. |
(12) | A match contains an XPath string that describes an aspect of the expected element. |
Example 1.1. illustrates a minimal action: its takes no inputs, outputs nothing either. It could however, depending on what is in the function stub, have an effect. For example, it could be used to increment a hit counter on a web page. If no description is provided, it is not possible to tell what an action does, especially if the action name is not explicit.
Example 1.2. presents a typical action. A description is provided in English and in French. We notice that both inputs have a 'use' attribute: this is because we do not want to reuse the same header over and over again to produce an endless suite of identical mails. The match elements used in the prototype are self explanatory.
Example 1.2. the make_mail action prototype
<action name='make_mail' func='make_mail_f'> <description lang='en'>Builds an email element given a header element and an email body</description> <description lang='fr'>Construit un élément email à partir d'un élément header (en-tête) et d'un élément body (corps du message)</description> <input used='yes'> <match>headers</match> </input> <input used='yes'> <match>body</match> </input> <output> <match>email</match> </output> </action>
Example 1.3. shows a complex action: two out three input arguments are optional and the match for the first argument is much more elaborated than those we have seen so far. If you are not yet familiar with XPath, here is what it means: we are looking for a http-request element with at least two child nodes. One of these children must be a url element and have a PCDATA child, and the other one must be a header element and have a PCDATA child.
Example 1.3. the http_get_ext action prototype
<action name='http_get_ext' func='http_get_ext_f'> <description lang='en'>Fetches a page on the web using an optional proxy, and optionally filtering spam out</description> <description lang='fr'>Ramène une page depuis le Web, en passant par un proxy (optionnel), et en supprimant le spam (optionnel)</description> <input> <match>http-request[url/text()][headers/text()]</match> </input> <input optional="yes"> <match>proxy[@type='http']</match> </input> <input optional="yes"> <match>spam-policy</match> </input> <output> <match>http-response</match> </output> </action>'''
An action stub is a python function. Methods will not work, because there is no way to pass the object along with the call[1]. This function will be called passing one and only one argument, which will be a DOM object implementing the Element interface.
The node name of this element is arguments and it contains the different arguments as child nodes, in an arbitrary order. If the prototype of the action specified an id attribute for an input, then the arguments matching this input will have an input-id attribute with the same value.
The next section will provide some information on how the stub can get the different arguments, and know which argument goes with which input.
Except for very simple cases where the action takes no inputs, one of the first thing the stub will do is extract the inputs passed to the action from the argument passed to the stub. There are several ways to do so, depending on the number of inputs expected, and how different the inputs are.
This is the easiest case (apart from no inputs to retrieve, of course). We just have to access the first child of the argument that was passed to the function. This is done with the firstChild member variable of the node passed as an argument to the stub (see Example 1.4.).
Example 1.4. Retrieving a single input
Given the following action prototype:
<action name='dance_boogie-woogie' function='dance_boogie-woogie_f'> <input> <match>tempo</match> </input> </action>
We could write the following code in the stub to store the tempo element in tempo_node:
def dance_boogie-woogie_f(arg) tempo_node = arg.firstChild # do some stuff with it now
A good way to do this is to iterate through the children, take a glance at the discriminatory factors and decide what we want to do with the node. This is most easy when each input can be recognized by looking at a single part of the node, for example the nodeName (see Example 1.5.) or a single attribute (see Example 1.6.) .
Example 1.5. Retrieving a multiple inputs, based on nodeNames
Given the following action prototype:
<action name='boris_vian' function='boris_vian_f'> <input> <match>red-grass</match> </input> <input> <match>autumn-in-pekin</match> </input> <input> <match>pianocktail</match> </input> </action>
We could write the following code in the stub to store the red-grass element in red_grass_node, etc.:
def boris_vian_f(arg) for child in arg.childNodes: if child.nodeName == 'red-grass': red_grass_node = child elif child.nodeName == 'autumn-in-pekin': autumn_in_pekin_node = child elif child.nodeName == 'pianocktail': pianocktail_node = child # do some stuff with it now
Example 1.6. Retrieving multiple inputs, based on an attribute value
Given the following action prototype:
<action name='farm' function='farm_f'> <input> <match>animal[@name='cow']</match> </input> <input> <match>animal[@name='pig']</match> </input> <input> <match>farmer[@name='Joe']</match> </input> </action>
We could write the following code:
def farm_f(arg) for child in arg.childNodes: attr = child.getAttributeNS('','name') if attr == 'cow': cow_node = child elif attr == 'pig': pig = child elif attr == 'Joe': farmer_node = child # do some stuff with it now
Since the farmer element also had an name attribute, we did not use the nodeName as a discriminating factor.
Sometimes, it is just too painful to iterate through the childNodes of the stub's argument. In any case, this is certainly not the part of the program that is the most interesting to code. For those of you who want a generic and easy way to get all the arguments you want this is for you. Be warned that this method has a cost, however, especially in terms of speed. The solution is to use the match elements used to describe your input. To do this, you can use two utility functions select_one() and select_many provided in Narval.lib. An illustration of this case can be found in Example 1.7.. Another case when this method proves very useful is when dealing with input lists and optional inputs. This is demonstrated in Example 1.8. and Example 1.9.
Example 1.7. Retrieving complex inputs
Given the following action prototype:
<action name='farm' function='farm_f'> <input> <match>animal[@species='cow']</match> </input> <input> <match>animal[@species='pig']</match> </input> <input> <match>farmer[@name='Joe']</match> </input> </action>
We could write the following code:
from Narval.lib import select_one def farm_f(arg) cow_node = select_one(arg,"animal[@species='cow']") pig_node = select_one(arg,"animal[@species='pig']") farmer_node = select_one(arg,"farmer[@name='Joe']") # do some stuff with it now
Example 1.8. Retrieving optional inputs
Given the following action prototype:
<action name='farm' function='farm_f'> <input> <match>animal[@species='cow']</match> </input> <input> <match>animal[@species='pig']</match> </input> <input optional='yes'> <match>farmer[@name='Joe']</match> </input> </action>
We could write the following code:
from Narval.lib import select_one def farm_f(arg) cow_node = select_one(arg,"animal[@species='cow']") pig_node = select_one(arg,"animal[@species='pig']") try: farmer_node = select_one(arg,"farmer[@name='Joe']") except SelectException: farmer_node = None # do some stuff with it now
Example 1.9. Retrieving inputs that can be lists
Given the following action prototype:
<action name='farm' function='farm_f'> <input list='yes'> <match>animal</match> </input> <input> <match>farmer</match> </input> </action>
We could write the following code:
from Narval.lib import select_one,select_many def farm_f(arg) animal_nodes = select_many(arg,"animal") farmer_node = select_one(arg,"farmer[@name='Joe']") # do some stuff with it now
In this example animal_nodes holds a python list of nodes that match the XPath. If the input matching animal had been declared optional, this list could have been empty.
If the prototype of the action uses id attributes on some of the inputs, these arguments matching these inputs will have an input-id attribute set to the same value as the id attribute of the input. This makes it very easy to recognise the different arguments, using either xpath expressions or an examination of all the child nodes of the arguments node.
Well basically, you can do anything you want here: create XML nodes, read and write files on hard disk,
There is a issue with interprocess communication using sockets within actions. As far as an action can open a connection, do some processing with it and close it by itself, everything goes fine. Things get a bit more difficult when a connection is to be shared between generic actions. Such actions are expected to take a socket input. A socket has a port attribute giving the port on which the communication is taking place. The action should connect to this port on localhost to communicate with the outside. Here is some sample code taken from Http.Write_back_to_socket:
## ## Write back to socket ## def Write_back_to_socket_f(args) : sock = select_one(args,'socket') hi = select_one(args,'http-response') import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect('localhost',int(sock.getAttributeNS(AL_NS,'port'))) s.send('HTTP/1.0 '+hi.getAttributeNS(AL_NS,'errcode')+' '+\ hi.getAttributeNS(AL_NS,'errmsg')+'\r\n') data = select_one(hi,'headers/text()').data s.send(data) if data[-1] != '\n' : s.send('\r\n\r\n') else: s.send('\r\n') try: s.send(select_one(hi,'content/text()').data) except SelectException: pass s.send('\r\n') s.close() return args.ownerDocument.createElement('outputs') MOD_XML=MOD_XML+''' <action name='Write_back_to_socket' func='Write_back_to_socket_f'> <description lang="en">Send back response to client</description> <description lang="fr">Renvoie la réponse au client</description> <input use="yes"><match>http-response</match></input> <input><match>socket</match></input> </action>'''
(1) | We start by retrieving the socket element as described previously. This element has a port attribute which tells us on which port we can communicate with the socket forwarder |
(2) | Then we create a python socket object, using the standard method… |
(3) | …and we connect it to localhost:port, which is the address of the socket forwarder. |
(4) | We can then use the socket normally. Depending on the action, we shell send or recv data. |
(5) | One we have finished, it's time to close the connection. Do not forget this step. |
(6) | As for any action, we return an outputs element. In this case, the action has no declared outputs, so the element is empty. Refer to Section 1.2.4. for more information. |
You can create new XML nodes and documents within a stub. New nodes are created using the ownerDocument member variable of the argument passed to the stub as a node factory. In order to avoid leaking memory, it is a good thing to attach the new node to the document, or to release it using the ReleaseNode function in xml.dom.ext when you no longer need it. Even better, do both (see Example 1.10.), so that if the stubs generates an exception before the element is released, Narval will be able to cope with the element.
The outputs element that is returned by the stub (See Section 1.2.4.) should not have a parent node.
More examples of node creation can be found in Section 1.2.4..
Example 1.10. Avoiding memory leaks with DOM
def myFunction_f(argument): # argument extraction code skipped # creating a new element doc = argument.ownerDocument new_element = doc.createElementNS(None,'element') # attach the element to the document doc.documentElement.appendChild(new_element) # some processing skipped # release the element from xml.dom.ext import ReleaseNode ReleaseNode(new_element)
Obviously, once the processing is done, we want to return a result. Narval expects to find an outputs (mind the 's' at the end) element containing the elements specified in the action prototype. This element is the only one that the action creates and that should not be appended to a parent node. There are two main ways of creating this element. The first one is illustrated in Example 1.11.: it consists of building the element nodes and text nodes and attributes one by one, and attaching them at the right place before returning them. The second one is shown in Example 1.12., and can be less tedious to code: you just write an XML string and turn it into a DOM document fragment using FromXml in xml.dom.ext.reader.Sax2.
Example 1.11. building the outputs with an XML string
<action name='pastry' function='pastry_f'> <output> <match>muffin</match> </output> </action>
We could write the following code:
def pastry_f(arg) # no inputs to read # we choose the flavour of the muffin from random import choice flavour = choice(['pumpkin','raisin', 'blueberry']) # build the output doc = arg.ownerDocument outputs = doc.createElementNS(None, 'outputs') muffin = doc.createElementNS(None, 'muffin') muffin.setAttributeNS('','flavour',flavour) outputs.appendChild(muffin) # return the result return outputs
If we suppose that the choice function returned the first element of the list, the output we built there is equivalent to the following XML string:
<outputs> <muffin flavour='pumpkin'/> </outputs>
Example 1.12. building the outputs by hand
Given the following action prototype:
<action name='user_desc' function='user_desc_f'> <output> <match>description</match> </output> </action>
We could write the following code:
def user_desc_f(arg) # no inputs to read # build the output doc = arg.ownerDocument outputs_string = """<outputs> <description> <firstname>Alexandre</firstname> <surname>Fayolle</surname> <birth encoding='iso'>19740728</birth> <height unit='cm'>183</height> <weight unit='kg'>85</weight> </description> </outputs>""" from xml.dom.ext.reader.Sax2 import FromXml fragment = FromXml(outputs_string,doc) # the first child of the fragment is the outputs node # we create a new node and transfer the contents in that node # so that we can release the fragment in a few lines of code outputs = doc.createElementNS(None, 'outputs') outputs.appendChild(fragment.firstChild.childNodes) # the fragment is now an almost empty shell. # We do not need it any more, so we release it from xml.dom.ext import ReleaseNode ReleaseNode(fragment) # return the result return outputs
[1] | Well, at least, this is what we believe. If there is a python hack that would do the trick, it's fine, but unless it is a very clean hack, we do not intend to support it. |
A naive way to define a module is saying that it is an action container. While this is true, there is also much more to modules than that. For one, functions in a module should have something in common, so a module is more something like an action library or a tool box. Furthermore, Section 4.2. will present yet another aspect of modules.
Building a new module is quite easy. In the python module where all the actions are declared, you must have a global variable called MOD_XML that contains all the XML declaration for the module and the actions. The usual way to do this is by initializing the variable at the beginning of the file and building incrementally as function stubs are declared:
MOD_XML='''<module name='Http'>''' ## ## Http Get Ext ## def http_get_ext_f(args): pass # function code intentionally skipped MOD_XML = MOD_XML+''' <action name='http_get_ext' func='http_get_ext_f'> <input> <match>http-request[url/text()][headers/text()]</match> </input> <input optional="yes"> <match>proxy[@type='http']</match> </input> <input optional="yes"> <match>spam-policy</match> </input> <output> <match>http-response</match> </output> </action>''' ## ## Write back to socket ## def Write_back_to_socket_f(args) : pass # function code intentionally skipped MOD_XML=MOD_XML+''' <action name='Write_back_to_socket' func='Write_back_to_socket_f'> <description lang="en">Send back response to client</description> <description lang="fr">Renvoie la réponse au client</description> <input use="yes"> <match>http-response</match> </input> <input> <match>socket</match> </input> </action>''' MOD_XML=MOD_XML+'</module>'
This makes it very easy to add new functions to a module, since you only have to add the code in the file and add the action prototype to MOD_XML.
Choosing what to put in an action is difficult. This is really the same challenge as designing a software library. With Narval however, a new factor comes in play. Actions can be used in recipes. A typical recipe should use from three to a dozen actions to perform its task, so actions should not have a too small granularity. Actions are supposed to perform elementary tasks at the scale of the recipe, which itself is very high level, so actions are already quite high level. A typical recipe will for instance manage an address book, so the required actions for such a recipe would be adding or removing an address. Proposing an action to read the address book from hard disk is too fine grained.
As always, everything is a matter of context, there may be recipes which require reading files from disk, and an action that does just that is provided in the standard distribution of Narval.
This means that something that can be a good candidate for an action in a module can be a bad one in another module. Writing test recipes is a good way to tell if the actions in a module are too low-level: if you get the impression that you are writing a program in a programing language like Python, Java or Younameit, then you probably got it wrong. Narval's ultimate goal is to bring the Power of computers in the hand of the average person in the street[2], so using your actions to write a recipe should not become something like programming. When you add a new action to a module, always ask yourself whether you would really like to have it in a recipe. It is much better to share code between the implementation of action stubs than to add an action that will have to be inserted in every recipe that uses actions from the module.
Deciding to pack actions in modules is one thing, deciding how to pack them is another one. The logical decision is to group them around a common theme. Since all actions in a module share a common file [3], this encourages sharing utility code between action stubs. In other words, all function within a module file need not be action stubs. There can be any number of helper functions provided in a module to avoid code duplication in action stubs, and ease the coding of new actions.
Each module may want to deal with it's own set of XML elements. Before introducing a new tag, you should carefully consider existing tags and see if one of them would be fine for what you need. If not so, you should write a DTD for the new element and decide of a namespace to hold it.
There is no support for namespaces in the current version of Narval, so expect some surprises. For now, it is better to choose your names carefully to avoid clashes.
There is no support for DTDs in the current version of Narval. However, providing a DTD serves two purposes: first, you will be ready when support for DTD is released, and second, a DTD, especially if it is commented, since you cannot specify everything within a DTD, can greatly help using your module. Some day in the near future, when XML Schema is released, we shall add support for it.
Before trying to test your module with Narval, you should perform some unitary testing, that will enable you to check that your module will behave as expected, or at least that it will not crash in a stupid way when loaded. This section introduces some testing techniques that isolates the module from Narval and thus make it easier to find some bugs. They especially enable the use of a python debugger and other standard debugging methods, which are rather difficult to set up when Narval is running.
One of the first thing you want to check in a module is that the prototypes of the actions are syntactically correct, since this will prevent Narval from being able to load the module. This is done by adding a python main function that will parse the string held by MOD_XML:
if __name__ == "__main__" : print MOD_XML from xml.dom.ext.reader import Sax2 doc=Sax2.FromXml(MOD_XML) print doc.documentElement
Once this is done, you can run your module from the command line as you would for any other program. This can bring up two kinds of errors:
Unfortunately, it is not possible to test all actions outside of Narval. If an action uses the socket forwarder, for instance, it will not be possible to test it if nothing is listening on the socket forwarder port, for instance. Similarly, if a group of actions are very tightly coupled, for instance if they share a common object and behave differently according to some internal state of the object, testing will be difficult.
The proposed method for testing is to call the stub of the action by passing it elements as Narval would do it. The outputs of the method can be retrieved and compared to what is expected, and thus the action is validated.
Writing modules is a Good Thing. Making them available for everyone is a Better Thing. So now that your module is coded and tested, now that you have sample recipes illustrating how to use your actions, it's release time!
Maybe you have so far coded for your eyes only. You know your code, how it works, and that's good. However, maybe a bit of tidying would be welcome.
We have seen in Section 1.1.1. that the description is optional. It is strongly recommended that you should use it for every action you write. This is one of the three indications a recipe programmer will have about what your action does, the other two being the module name and the action name. As the module and the action name are one or two words, this leaves only the description for something a bit more consistent. Keep in mind that due to screen space limitations, it is best to keep the description string as a one-liner. Avoid if possible repeating what can be guessed by looking at the XML prototype, and rather elaborate about the action that takes place to transform the inputs into outputs.
The standard python documentation advices apply here. Use doc strings wherever applies, use comments where needed. We believe at Logilab that it is worth spending a lot of time on the code so that it can be understood without using too many comments. This includes using good variable names and good function names, rewriting shaggy code again and again until it becomes clean, using standard Design Patterns and naming the objects accordingly. We encourage you to do the same, for the benefit of everyone.
We have chosen to release Narval under GPL. We do not wish to impose anything on the developer community about the modules they contribute, so you are free to distribute your modules under the license you wish. However, we encourage you to use a well known GPL-compatible license. This will enable us to redistribute your modules in future Narval releases, and to make it available on our web site without worrying about possible legal problems. Please include a license statement with the modules you release.
[2] | and when we say Power, we mean Real Power, and not just surfing the web and downloading WaReZ and thinking we are now 31337 hackers. Computers are good because they can save time by doing boring stuff for you. Using a word processor is not a progress over using a type machine if you have to open each of the 456 files in your directory to manually change the logo of your company on the first page. Most people who use computers nowadays will have to do it that way, though. Narval should enable them to quickly write a recipe that will do this automatically. |
[3] | This is the case right now. It might be possible to build modules that would be stored in directories, with the MOD_XML variable initialized in the __init__.py file. It has not been tested yet, but if it is possible, we shall provide support for this in a future release |
In Narval, steps can be either a recipe or an action or a transformation. In this chapter, we will focus on the transformation case.
During initialization, Narval loads transformations in its memory (just as it loads actions). They can then be used by recipes.
Just as actions are stored in modules, transformations are stored in groups. A group is a sub-directory of transform in the Narval_HOME, that contains various transformation files. These files have an xslt extension.
This means that only the files of these directories that have an xslt extension will be considered as transformations and loaded in memory. Other files will simply be ignored.
For example the file stored in transforms/MyGroup/MyTransf.xslt (local path from Narval_HOME) will be loaded in memory as an element transform whose group is MyGroup and name is MyTransf. This transformation can then be pointed to by a step in a recipe if the step target is set to MyGroup.MyTransf.
Transformations are in fact XSL Tranformations (see the W3C specification at http://www.w3.org/TR/xslt). As they are designed to turn an XML tree into another XML tree, their root node must be an xsl:transform element (the xsl:stylesheet root node is reserved for stylesheets that deal with layout and display).
For a correct processing in Narval, the XSLT must have a specific node that describe their prototype. Thus, the xsl:transform root node has a prototype child node. This node is the same as the ones that are found in the action description: it has various input nodes with match child nodes that describe the elements that are processed by the transformation and various output nodes with match child nodes that describe similarly the elements returned by the transformation.
Below is an example of a very simple XSLT that turns lead into gold:
<?xml version="1.0"?> <xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <prototype> <input><match>lead</match></input> <ouput><match>gold</match></output> </prototype> <xsl:output method="xml"/> <xsl:template match="lead"> <gold> <xsl:copy-of select="@*"/> <!-- copies all the attributes --> <xsl:copy-of select="*"/> <!-- copies all the child nodes --> </gold> </xsl:template> </xsl:transform>
As in the previous example, XSLT must use the xml output method because the result of the transformation will be appended in memory as an XML element.
As for actions, Narval passes to the XSL Transformation an XML tree that has the following form:
<arguments> <!-- the Narval memory elements that match the input prototype --> <!-- of the XSL Transformation. --> </arguments>
For instance, in the alchemical previous example, the tree passed to the transformation would have been:
<arguments> <lead/> </arguments>
The XML tree returned by the XSL Transformation contains directly the various nodes that must be matched with the output prototype. As the ouput of an XSL Transformation can be non-valid XML, it can have various root nodes (e.g. one node for each expected ouput).
The XML nodes returned by the XSL Transformation mustn't be included in a arguments node such as inputs are.
In our alchemical example, the output tree of the transformation is:
<gold/>
Thanks to the xsl:import and xsl:include elements, it is possible to import or include other transformations in a given transformation. This is very useful when you are trying to set generic transformations in a complex problem. If you want to use these mechanisms, you should read carefully the XSLT specification to learn the difference between an import and an include.
The import and include mechanism has been slightly modified to be compliant with Narval specificities in transformations storing.
Thus, when the href attribute of an xsl:import or an xsl:include contains a file name as name1/name2.xslt, Narval will search for the referred transformation in its memory as one that belongs to the name1 group and has the name2 name.
When the href contains a file name as name1/name2.ext with ext any extension different from xslt, Narval will search for the referred transformation on the disk in the file transforms/name1/name2.ext (local path from NARVAL_HOME).
In the other cases, the href content is solved as a classic URI.
As the .xslt files are read as transformations files, they must contain a prototype element and be semantically readable as a transformation i.e. something that turns an element of Narval memory into another element of Narval memory.
Sometimes, it is interesting to include or import in an XSL Transformation some files that contain templates used by various other transformations. These files do not contain transformations (in Narval sense) and thus should not have an xslt extension. An nice trick is to give them another extension such as xsl.
The XSLT specification describes a mechanism for setting global xsl:param and passing them new values when processing the transformation. This mechanism does not work in Narval. Instead, you should use arguments element to modify the values of your parameters.
For example, suppose your XSL Transformation has a global parameter named sender that contains your name. The XSLT should then start with:
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <prototype> ... </prototype> <xsl:output method="xml"/> <xsl:param name="sender">Narval master</xsl:param> <!-- Sets the "sender" param to a default value --> ... </xsl:transform>
To be able to change the value of this parameter from within Narval, you should change the XSLT to:
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <prototype> ... </prototype> <xsl:output method="xml"/> <xsl:param name="sender">Narval master</xsl:param> <!-- Sets the "sender" param to a default value --> <xsl:template match="sender"> <xsl:param name="sender"><xsl:value-of select='.'/></xsl:param> <!-- Sets the "sender" param value to the content of the sender node --> </xsl:template> <xsl:template match="arguments"> <xsl:apply-templates select="sender"/> <xsl:apply-templates select="*"/> </xsl:template> ... </xsl:transform>When you are calling the transformation in your recipe, you should then pass a sender node in the arguments of the step:
<recipe ...> ... <step type="transform" target="MyGroup.MyTransf"> <arguments> <sender>John Smith</sender> </arguments> </step> ... </recipe>
Narval uses 4Suite's XSLT processor. Therefore, if necessary, you can use the extensions this processor provides. You should be extremly aware that using extensions in an XSL Transformation makes these transformations dependant of the processor you use. This could be problematic if you want your transformation to be usable anywhere by anyone.
The extensions of 4Suite XSLT processor are described at http://services.4Suite.org/documents/4Suite/4XSLT-Extensions. All these extensions can be used in Narval.
There is no obligation of having a one to one correspondence between actions and stubs. It is perfectly acceptable to have a single stub that would behave differently according to the inputs is received. A typical example would be an action that acts as a proxy to some outer program using the same interface provided by the program. One could argue that it could be possible to have an single action with optional inputs that would be associated with the stub. This is true, but would nevertheless not be a good idea, because it would make recipes much less easy to read, whereas providing several actions with distinct and clear names can make things much easier to understand.
Something great about modules is that they can behave as interfaces to programs, and as any OO programmer will tell you, Interfaces are Good Things. For instance, it is possible to identify the basic requirements for a mail module. However, the implementation of the stubs depend on the underlying operating system: under Unix, mail is often read in /var/spool/mail or another system mailbox, whereas Windows users generally use a POP3 or IMAP server. Yet, from an action point of view, they all receive and send mails, sometimes with attached documents, and that's about it. Once the module interface has been defined, it is possible to choose which implementation of the module should be installed on a given system, and all the recipes will keep on working regardless of the implementation.