view Sphinx/source/users/lua.rst @ 1:1b6f12b8a54b

license in footer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 22 Apr 2016 14:18:42 +0200
parents 901e8961f46e
children c98317fedf87
line wrap: on
line source

.. _lua:

Server-side scripting with Lua
==============================

.. contents::

Since release 0.5.2, Orthanc supports server-side scripting through
the `Lua <http://en.wikipedia.org/wiki/Lua_(programming_language)>`__
scripting language. Thanks to this major feature, Orthanc can be tuned
to specific medical workflows without being driven by an external
script. This page summarizes the possibilities of Orthanc server-side
scripting.

Many other examples are `available in the source distribution
<https://bitbucket.org/sjodogne/orthanc/src/default/Resources/Samples/Lua/>`__.


Installing a Lua Script
-----------------------

.. highlight:: bash

A custom Lua script can be installed either by the :ref:`configuration
file <configuration>`, or by uploading it
through the :ref:`REST API <rest-samples>`.

To install it by the **configuration file** method, you just have to
specify the path to the file containing the Lua script in the
``LuaScripts`` variable.

To upload a script stored in the file "``script.lua``" through the
**REST API**, use the following command::

    $ curl -X POST http://localhost:8042/tools/execute-script --data-binary @script.lua

Pay attention to the fact that, contrarily to the scripts installed
from the configuration file, the scripts installed through the REST
API are non-persistent: They are discarded after a restart of Orthanc,
which makes them useful for script prototyping. You can also interpret
a single Lua command through the REST API::

    $ curl -X POST http://localhost:8042/tools/execute-script --data-binary "print(42)"

*Note:* The ``--data-binary`` cURL option is used instead of
``--data`` to prevent the interpretation of newlines by cURL, which is
`mandatory for the proper evaluation
<http://stackoverflow.com/q/3872427/881731>`__ of the possible
comments inside the Lua script.


Lua API
-------


.. _lua-callbacks:

Callbacks to react to events
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The Lua engine of Orthanc comes invokes the following callbacks that
are triggered on various events. Here are the **generic events**:

* ``function Initialize()``: Invoked as soon as the Orthanc server is started.
* ``function Finalize()``: Invoked just before the Orthanc server is stopped.

Some **permission-related events** allow to filter incoming requests:

* ``function ReceivedInstanceFilter(dicom, origin)``:
  Invoked to known whether an incoming DICOM instance should be
  accepted. :ref:`See this section <lua-filter-dicom>`. The ``origin``
  parameter is :ref:`documented separately <lua-origin>`.
* ``function IncomingHttpRequestFilter(method, uri, ip, username,
  httpHeaders)``: Invoked to known whether a REST request should be
  accepted. :ref:`See this section <lua-filter-rest>`.

Some **DICOM-related events** allow to react to the reception of
new medical images:

* ``function OnStoredInstance(instanceId, tags, metadata, origin)``:
  Invoked whenever a new instance has been stored into Orthanc. 
  This is especially useful for :ref:`lua-auto-routing`. The ``origin``
  parameter is :ref:`documented separately <lua-origin>`.
* ``function OnStablePatient(patientId, tags, metadata)``: Invoked
  whenever a patient has not received any new instance for a certain
  amount of time (cf. the option ``StableAge`` in the
  :ref:`configuration file <configuration>`). The :ref:`identifier
  <orthanc-ids>` of the patient is provided, together with her DICOM
  tags and her metadata.
* ``function OnStableSeries(seriesId, tags, metadata)``: Invoked
  whenever a series has not received any new instance for a certain
  amount of time.
* ``function OnStableStudy(studyId, tags, metadata)``: Invoked
  whenever a study has not received any new instance for a certain
  amount of time.
* ``function IncomingFindRequestFilter(source, origin)``: Invoked
  whenever Orthanc receives an incoming C-Find query through the DICOM
  protocol. This allows to inspect the content of the C-Find query,
  and possibly modify it if a patch is needed for some manufacturer. A
  `sample script is available
  <https://bitbucket.org/sjodogne/orthanc/src/default/Resources/Samples/Lua/IncomingFindRequestFilter.lua>`__.

Furthermore, whenever a DICOM association is negociated for C-Store
SCP, several callbacks are successively invoked to specify which
**transfer syntaxes** are accepted for the association. These
callbacks are listed in `this sample script
<https://bitbucket.org/sjodogne/orthanc/src/default/Resources/Samples/Lua/TransferSyntaxEnable.lua>`__.

*Note:* All of these callbacks are guaranteed to be **invoked in
mutual exclusion**. This implies that Lua scripting in Orthanc does
not support any kind of concurrency.


.. _lua-rest:

Calling the REST API of Orthanc
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Lua scripts have :ref:`full access to the REST API <rest>` of Orthanc
through the following functions:

* ``RestApiGet(uri, builtin)``
* ``RestApiPost(uri, body, builtin)``
* ``RestApiPut(uri, body, builtin)``
* ``RestApiDelete(uri, builtin)``

The ``uri`` arguments specifies the URI against which to make the
request, and ``body`` is a string containing the body of POST/PUT
request.  The ``builtin`` parameter is an optional Boolean that
specifies whether the request targets only the built-in REST API of
Orthanc (if set to ``true``), or the full the REST API after being
tainted by the plugins (if set to ``false``).


General-purpose functions
^^^^^^^^^^^^^^^^^^^^^^^^^

The Lua engine of Orthanc contain several general-purpose ancillary
functions:

* ``PrintRecursive(v)`` recursively prints the content of a `Lua table
  <http://www.lua.org/pil/2.5.html>`__ to the log file of Orthanc.
* ``ParseJson(s)`` converts a string encoded in the `JSON format
  <https://en.wikipedia.org/wiki/JSON>`__ to a Lua table.
* ``DumpJson(v, keepStrings)`` encodes a Lua table as a JSON string.
  Setting the optional argument ``keepStrings`` (available from
  release 0.9.5) to ``true`` prevents the automatic conversion of
  strings to integers.
* ``GetOrthancConfiguration()`` returns a Lua table containing the
  content of the :ref:`configuration files <configuration>` of
  Orthanc.


Similarly to the functions to :ref:`call the REST API of Orthanc
<lua-rest>`, several functions are available to make generic HTTP
requests to Web services:

* ``HttpGet(url)``
* ``HttpPost(url, body)``
* ``HttpPut(url, body)``
* ``HttpDelete(url)``
* ``SetHttpCredentials(username, password)`` can be used to setup the
  HTTP credentials.


.. _lua-origin:

Origin of the instances
^^^^^^^^^^^^^^^^^^^^^^^

Whenever Orthanc decides whether it should should store a new instance
(cf. the ``ReceivedInstanceFilter()`` callback), or whenever it has
actually stored a new instance (cf. the ``OnStoredInstance``
callback), an ``origin`` parameter is provided. This parameter is a
`Lua table <http://www.lua.org/pil/2.5.html>`__ that describes from
which Orthanc subsystem the new instance comes from.

There are 4 possible subsystems, that can be distinguished according
to the value of ``origin["RequestOrigin"]``:

* ``RestApi``: The instance originates from some HTTP request to the REST
  API. In this case, the ``RemoteIp`` and ``Username`` fields are
  available in ``origin``. They respectively describe the IP address
  of the HTTP client, and the username that was used for HTTP
  authentication (as defined in the ``RegisteredUsers``
  :ref:`configuration variable <configuration>`).
* ``DicomProtocol``: The instance originates from a DICOM C-Store.
  The fields ``RemoteIp``, ``RemoteAet`` and ``CalledAet``
  respectively provide the IP address of the DICOM SCU (client), the
  application entity title of the DICOM SCU client, and the
  application entity title of the Orthanc SCP server. The
  ``CalledAet`` can be used for :ref:`advanced auto-routing scenarios
  <lua-auto-routing>`, when a single instance of Orthanc acts as a
  proxy for several DICOM SCU clients.
* ``Lua``: The instance originates from a Lua script.
* ``Plugins``: The instance originates from a plugin.


.. _lua-filter-dicom:

Filtering Incoming DICOM Instances
----------------------------------

.. highlight:: lua

Each time a DICOM instance is received by Orthanc (either through the
DICOM protocol or through the REST API), the
``ReceivedInstanceFilter()`` Lua function is invoked. If this callback
returns ``true``, the instance is accepted for storage. If it returns
``false``, the instance is discarded. This mechanism can be used to
filter the incoming DICOM instances. Here is an example of a Lua
filter that only allows incoming instances of MR modality::

 function ReceivedInstanceFilter(dicom, origin) 
    -- Only allow incoming MR images   
    if dicom.Modality == 'MR' then
       return true 
    else
       return false
    end
 end

The argument dicom corresponds to a `Lua table
<http://www.lua.org/pil/2.5.html>`__ (i.e. an associative array) that
contains the DICOM tags of the incoming instance. For debugging
purpose, you can print this structure as follows::

 function ReceivedInstanceFilter(dicom, origin) 
    PrintRecursive(dicom)
    -- Accept all incoming instances (default behavior)
    return true 
 end

The argument ``origin`` is :ref:`documented separately <lua-origin>`.


.. _lua-filter-rest:

Filtering Incoming REST Requests
--------------------------------

.. highlight:: lua

Lua scripting can be used to control the access to the various URI of
the REST API. Each time an incoming HTTP request is received, the
``IncomingHttpRequestFilter()`` Lua function is called. The access to
the resource is granted if and only if this callback script returns
``true``.

This mechanism can be used to implement fine-grained `access control
lists <http://en.wikipedia.org/wiki/Access_control_list>`__. Here is
an example of a Lua script that limits POST, PUT and DELETE requests
to an user that is called "admin"::

 function IncomingHttpRequestFilter(method, uri, ip, username, httpHeaders)
    -- Only allow GET requests for non-admin users
 
   if method == 'GET' then
       return true
    elseif username == 'admin' then
       return true
    else
       return false
    end
 end

Here is a description of the arguments of this Lua callback:

* ``method``: The HTTP method (GET, POST, PUT or DELETE).
* ``uri``: The path to the resource (e.g. ``/tools/generate-uid``).
* ``ip``: The IP address of the host that has issued the HTTP request (e.g. ``127.0.0.1``).
* ``username``: If HTTP Basic Authentication is enabled in the
  :ref:`configuration file <configuration>`, the name of the user that
  has issued the HTTP request (as defined in the ``RegisteredUsers``
  configuration variable). If the authentication is disabled, this
  argument is set to the empty string.
* ``httpHeaders``: The HTTP headers of the incoming request. This
  argument is available since Orthanc 1.0.1. It is useful if the
  authentication should be achieved through tokens, for instance
  against a `LDAP
  <https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol>`__
  or `OAuth2 <https://en.wikipedia.org/wiki/OAuth>`__ server.


.. _lua-auto-routing:

Auto-Routing of DICOM Images
----------------------------

.. highlight:: lua

Since release 0.8.0, the routing of DICOM flows can be very easily
automated with Orthanc. All you have to do is to declare your
destination modality in the :ref:`configuration file <configuration>`
(section ``DicomModalities``), then to create and install a Lua
script. For instance, here is a sample script::

    function OnStoredInstance(instanceId, tags, metadata)
      Delete(SendToModality(instanceId, 'sample'))
    end

If this script is loaded into Orthanc, whenever a new DICOM instance
is received by Orthanc, it will be routed to the modality whose
symbolic name is ``sample`` (through a Store-SCU command), then it
will be removed from Orthanc. In other words, this is a **one-liner
script to implement DICOM auto-routing**.

Very importantly, thanks to this feature, you do not have to use the
REST API or to create external scripts in order to automate simple
imaging flows. The scripting engine is entirely contained inside the
Orthanc core system.

Thanks to Lua expressiveness, you can also implement conditional
auto-routing. For instance, if you wish to route only patients whose
name contains "David", you would simply write::

 function OnStoredInstance(instanceId, tags, metadata)
    -- Extract the value of the "PatientName" DICOM tag
    local patientName = string.lower(tags['PatientName'])
 
   if string.find(patientName, 'david') ~= nil then
       -- Only route patients whose name contains "David"
       Delete(SendToModality(instanceId, 'sample'))
 
   else
       -- Delete the patients that are not called "David"
       Delete(instanceId)
    end
 end

Besides ``SendToModality()``, a mostly identical function with the
same arguments called ``SendToPeer()`` can be used to route instances
to :ref:`Orthanc peers <peers>`.  It is also possible to modify the
received instances before routing them. For instance, here is how you
would replace the ``StationName`` DICOM tag::

 function OnStoredInstance(instanceId, tags, metadata)
    -- Ignore the instances that result from a modification to avoid
    -- infinite loops
    if (metadata['ModifiedFrom'] == nil and
        metadata['AnonymizedFrom'] == nil) then
 
      -- The tags to be replaced
       local replace = {}
       replace['StationName'] = 'My Medical Device'
 
      -- The tags to be removed
       local remove = { 'MilitaryRank' }

      -- Modify the instance, send it, then delete the modified instance
       Delete(SendToModality(ModifyInstance(instanceId, replace, remove, true), 'sample'))

      -- Delete the original instance
       Delete(instanceId)
    end
 end


**Important remark:** The ``SendToModality()``, ``SendToPeer()``,
``ModifyInstance()`` and ``Delete()`` functions are for the most
common cases of auto-routing (implying a single DICOM instance, and
possibly a basic modification of this instance). For more evolved
auto-routing scenarios, remember that Lua scripts :ref:`have full to
the REST API of Orthanc <lua-rest>`, and that :ref:`other callbacks
are available <lua-callbacks>` to react to other events than the
reception of a single instance (notably ``OnStablePatient()``,
``OnStableStudy()`` and ``OnStableSeries()``).