0
|
1 .. _lua:
|
|
2
|
|
3 Server-side scripting with Lua
|
|
4 ==============================
|
|
5
|
|
6 .. contents::
|
|
7
|
|
8 Since release 0.5.2, Orthanc supports server-side scripting through
|
|
9 the `Lua <http://en.wikipedia.org/wiki/Lua_(programming_language)>`__
|
|
10 scripting language. Thanks to this major feature, Orthanc can be tuned
|
|
11 to specific medical workflows without being driven by an external
|
|
12 script. This page summarizes the possibilities of Orthanc server-side
|
|
13 scripting.
|
|
14
|
|
15 Many other examples are `available in the source distribution
|
|
16 <https://bitbucket.org/sjodogne/orthanc/src/default/Resources/Samples/Lua/>`__.
|
|
17
|
|
18
|
|
19 Installing a Lua Script
|
|
20 -----------------------
|
|
21
|
|
22 .. highlight:: bash
|
|
23
|
|
24 A custom Lua script can be installed either by the :ref:`configuration
|
|
25 file <configuration>`, or by uploading it
|
|
26 through the :ref:`REST API <rest-samples>`.
|
|
27
|
|
28 To install it by the **configuration file** method, you just have to
|
|
29 specify the path to the file containing the Lua script in the
|
|
30 ``LuaScripts`` variable.
|
|
31
|
|
32 To upload a script stored in the file "``script.lua``" through the
|
|
33 **REST API**, use the following command::
|
|
34
|
|
35 $ curl -X POST http://localhost:8042/tools/execute-script --data-binary @script.lua
|
|
36
|
|
37 Pay attention to the fact that, contrarily to the scripts installed
|
|
38 from the configuration file, the scripts installed through the REST
|
|
39 API are non-persistent: They are discarded after a restart of Orthanc,
|
|
40 which makes them useful for script prototyping. You can also interpret
|
|
41 a single Lua command through the REST API::
|
|
42
|
|
43 $ curl -X POST http://localhost:8042/tools/execute-script --data-binary "print(42)"
|
|
44
|
|
45 *Note:* The ``--data-binary`` cURL option is used instead of
|
|
46 ``--data`` to prevent the interpretation of newlines by cURL, which is
|
|
47 `mandatory for the proper evaluation
|
|
48 <http://stackoverflow.com/q/3872427/881731>`__ of the possible
|
|
49 comments inside the Lua script.
|
|
50
|
|
51
|
|
52 Lua API
|
|
53 -------
|
|
54
|
|
55
|
|
56 .. _lua-callbacks:
|
|
57
|
|
58 Callbacks to react to events
|
|
59 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
60
|
|
61 The Lua engine of Orthanc comes invokes the following callbacks that
|
|
62 are triggered on various events. Here are the **generic events**:
|
|
63
|
|
64 * ``function Initialize()``: Invoked as soon as the Orthanc server is started.
|
|
65 * ``function Finalize()``: Invoked just before the Orthanc server is stopped.
|
|
66
|
|
67 Some **permission-related events** allow to filter incoming requests:
|
|
68
|
|
69 * ``function ReceivedInstanceFilter(dicom, origin)``:
|
|
70 Invoked to known whether an incoming DICOM instance should be
|
|
71 accepted. :ref:`See this section <lua-filter-dicom>`. The ``origin``
|
|
72 parameter is :ref:`documented separately <lua-origin>`.
|
|
73 * ``function IncomingHttpRequestFilter(method, uri, ip, username,
|
|
74 httpHeaders)``: Invoked to known whether a REST request should be
|
|
75 accepted. :ref:`See this section <lua-filter-rest>`.
|
|
76
|
|
77 Some **DICOM-related events** allow to react to the reception of
|
|
78 new medical images:
|
|
79
|
|
80 * ``function OnStoredInstance(instanceId, tags, metadata, origin)``:
|
|
81 Invoked whenever a new instance has been stored into Orthanc.
|
|
82 This is especially useful for :ref:`lua-auto-routing`. The ``origin``
|
|
83 parameter is :ref:`documented separately <lua-origin>`.
|
|
84 * ``function OnStablePatient(patientId, tags, metadata)``: Invoked
|
|
85 whenever a patient has not received any new instance for a certain
|
|
86 amount of time (cf. the option ``StableAge`` in the
|
|
87 :ref:`configuration file <configuration>`). The :ref:`identifier
|
|
88 <orthanc-ids>` of the patient is provided, together with her DICOM
|
|
89 tags and her metadata.
|
|
90 * ``function OnStableSeries(seriesId, tags, metadata)``: Invoked
|
|
91 whenever a series has not received any new instance for a certain
|
|
92 amount of time.
|
|
93 * ``function OnStableStudy(studyId, tags, metadata)``: Invoked
|
|
94 whenever a study has not received any new instance for a certain
|
|
95 amount of time.
|
|
96 * ``function IncomingFindRequestFilter(source, origin)``: Invoked
|
|
97 whenever Orthanc receives an incoming C-Find query through the DICOM
|
|
98 protocol. This allows to inspect the content of the C-Find query,
|
|
99 and possibly modify it if a patch is needed for some manufacturer. A
|
|
100 `sample script is available
|
|
101 <https://bitbucket.org/sjodogne/orthanc/src/default/Resources/Samples/Lua/IncomingFindRequestFilter.lua>`__.
|
|
102
|
|
103 Furthermore, whenever a DICOM association is negociated for C-Store
|
|
104 SCP, several callbacks are successively invoked to specify which
|
|
105 **transfer syntaxes** are accepted for the association. These
|
|
106 callbacks are listed in `this sample script
|
|
107 <https://bitbucket.org/sjodogne/orthanc/src/default/Resources/Samples/Lua/TransferSyntaxEnable.lua>`__.
|
|
108
|
|
109 *Note:* All of these callbacks are guaranteed to be **invoked in
|
|
110 mutual exclusion**. This implies that Lua scripting in Orthanc does
|
|
111 not support any kind of concurrency.
|
|
112
|
|
113
|
|
114 .. _lua-rest:
|
|
115
|
|
116 Calling the REST API of Orthanc
|
|
117 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
118
|
|
119 Lua scripts have :ref:`full access to the REST API <rest>` of Orthanc
|
|
120 through the following functions:
|
|
121
|
|
122 * ``RestApiGet(uri, builtin)``
|
|
123 * ``RestApiPost(uri, body, builtin)``
|
|
124 * ``RestApiPut(uri, body, builtin)``
|
|
125 * ``RestApiDelete(uri, builtin)``
|
|
126
|
|
127 The ``uri`` arguments specifies the URI against which to make the
|
|
128 request, and ``body`` is a string containing the body of POST/PUT
|
|
129 request. The ``builtin`` parameter is an optional Boolean that
|
|
130 specifies whether the request targets only the built-in REST API of
|
|
131 Orthanc (if set to ``true``), or the full the REST API after being
|
|
132 tainted by the plugins (if set to ``false``).
|
|
133
|
|
134
|
|
135 General-purpose functions
|
|
136 ^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
137
|
|
138 The Lua engine of Orthanc contain several general-purpose ancillary
|
|
139 functions:
|
|
140
|
|
141 * ``PrintRecursive(v)`` recursively prints the content of a `Lua table
|
|
142 <http://www.lua.org/pil/2.5.html>`__ to the log file of Orthanc.
|
|
143 * ``ParseJson(s)`` converts a string encoded in the `JSON format
|
|
144 <https://en.wikipedia.org/wiki/JSON>`__ to a Lua table.
|
|
145 * ``DumpJson(v, keepStrings)`` encodes a Lua table as a JSON string.
|
|
146 Setting the optional argument ``keepStrings`` (available from
|
|
147 release 0.9.5) to ``true`` prevents the automatic conversion of
|
|
148 strings to integers.
|
|
149 * ``GetOrthancConfiguration()`` returns a Lua table containing the
|
|
150 content of the :ref:`configuration files <configuration>` of
|
|
151 Orthanc.
|
|
152
|
|
153
|
|
154 Similarly to the functions to :ref:`call the REST API of Orthanc
|
|
155 <lua-rest>`, several functions are available to make generic HTTP
|
|
156 requests to Web services:
|
|
157
|
|
158 * ``HttpGet(url)``
|
|
159 * ``HttpPost(url, body)``
|
|
160 * ``HttpPut(url, body)``
|
|
161 * ``HttpDelete(url)``
|
|
162 * ``SetHttpCredentials(username, password)`` can be used to setup the
|
|
163 HTTP credentials.
|
|
164
|
|
165
|
|
166 .. _lua-origin:
|
|
167
|
|
168 Origin of the instances
|
|
169 ^^^^^^^^^^^^^^^^^^^^^^^
|
|
170
|
|
171 Whenever Orthanc decides whether it should should store a new instance
|
|
172 (cf. the ``ReceivedInstanceFilter()`` callback), or whenever it has
|
|
173 actually stored a new instance (cf. the ``OnStoredInstance``
|
|
174 callback), an ``origin`` parameter is provided. This parameter is a
|
|
175 `Lua table <http://www.lua.org/pil/2.5.html>`__ that describes from
|
|
176 which Orthanc subsystem the new instance comes from.
|
|
177
|
|
178 There are 4 possible subsystems, that can be distinguished according
|
|
179 to the value of ``origin["RequestOrigin"]``:
|
|
180
|
|
181 * ``RestApi``: The instance originates from some HTTP request to the REST
|
|
182 API. In this case, the ``RemoteIp`` and ``Username`` fields are
|
|
183 available in ``origin``. They respectively describe the IP address
|
|
184 of the HTTP client, and the username that was used for HTTP
|
|
185 authentication (as defined in the ``RegisteredUsers``
|
|
186 :ref:`configuration variable <configuration>`).
|
|
187 * ``DicomProtocol``: The instance originates from a DICOM C-Store.
|
|
188 The fields ``RemoteIp``, ``RemoteAet`` and ``CalledAet``
|
|
189 respectively provide the IP address of the DICOM SCU (client), the
|
|
190 application entity title of the DICOM SCU client, and the
|
|
191 application entity title of the Orthanc SCP server. The
|
|
192 ``CalledAet`` can be used for :ref:`advanced auto-routing scenarios
|
|
193 <lua-auto-routing>`, when a single instance of Orthanc acts as a
|
|
194 proxy for several DICOM SCU clients.
|
|
195 * ``Lua``: The instance originates from a Lua script.
|
|
196 * ``Plugins``: The instance originates from a plugin.
|
|
197
|
|
198
|
|
199 .. _lua-filter-dicom:
|
|
200
|
|
201 Filtering Incoming DICOM Instances
|
|
202 ----------------------------------
|
|
203
|
|
204 .. highlight:: lua
|
|
205
|
|
206 Each time a DICOM instance is received by Orthanc (either through the
|
|
207 DICOM protocol or through the REST API), the
|
|
208 ``ReceivedInstanceFilter()`` Lua function is invoked. If this callback
|
|
209 returns ``true``, the instance is accepted for storage. If it returns
|
|
210 ``false``, the instance is discarded. This mechanism can be used to
|
|
211 filter the incoming DICOM instances. Here is an example of a Lua
|
|
212 filter that only allows incoming instances of MR modality::
|
|
213
|
|
214 function ReceivedInstanceFilter(dicom, origin)
|
|
215 -- Only allow incoming MR images
|
|
216 if dicom.Modality == 'MR' then
|
|
217 return true
|
|
218 else
|
|
219 return false
|
|
220 end
|
|
221 end
|
|
222
|
|
223 The argument dicom corresponds to a `Lua table
|
|
224 <http://www.lua.org/pil/2.5.html>`__ (i.e. an associative array) that
|
|
225 contains the DICOM tags of the incoming instance. For debugging
|
|
226 purpose, you can print this structure as follows::
|
|
227
|
|
228 function ReceivedInstanceFilter(dicom, origin)
|
|
229 PrintRecursive(dicom)
|
|
230 -- Accept all incoming instances (default behavior)
|
|
231 return true
|
|
232 end
|
|
233
|
|
234 The argument ``origin`` is :ref:`documented separately <lua-origin>`.
|
|
235
|
|
236
|
|
237 .. _lua-filter-rest:
|
|
238
|
|
239 Filtering Incoming REST Requests
|
|
240 --------------------------------
|
|
241
|
|
242 .. highlight:: lua
|
|
243
|
|
244 Lua scripting can be used to control the access to the various URI of
|
|
245 the REST API. Each time an incoming HTTP request is received, the
|
|
246 ``IncomingHttpRequestFilter()`` Lua function is called. The access to
|
|
247 the resource is granted if and only if this callback script returns
|
|
248 ``true``.
|
|
249
|
|
250 This mechanism can be used to implement fine-grained `access control
|
|
251 lists <http://en.wikipedia.org/wiki/Access_control_list>`__. Here is
|
|
252 an example of a Lua script that limits POST, PUT and DELETE requests
|
|
253 to an user that is called "admin"::
|
|
254
|
|
255 function IncomingHttpRequestFilter(method, uri, ip, username, httpHeaders)
|
|
256 -- Only allow GET requests for non-admin users
|
|
257
|
|
258 if method == 'GET' then
|
|
259 return true
|
|
260 elseif username == 'admin' then
|
|
261 return true
|
|
262 else
|
|
263 return false
|
|
264 end
|
|
265 end
|
|
266
|
|
267 Here is a description of the arguments of this Lua callback:
|
|
268
|
|
269 * ``method``: The HTTP method (GET, POST, PUT or DELETE).
|
|
270 * ``uri``: The path to the resource (e.g. ``/tools/generate-uid``).
|
|
271 * ``ip``: The IP address of the host that has issued the HTTP request (e.g. ``127.0.0.1``).
|
|
272 * ``username``: If HTTP Basic Authentication is enabled in the
|
|
273 :ref:`configuration file <configuration>`, the name of the user that
|
|
274 has issued the HTTP request (as defined in the ``RegisteredUsers``
|
|
275 configuration variable). If the authentication is disabled, this
|
|
276 argument is set to the empty string.
|
|
277 * ``httpHeaders``: The HTTP headers of the incoming request. This
|
|
278 argument is available since Orthanc 1.0.1. It is useful if the
|
|
279 authentication should be achieved through tokens, for instance
|
|
280 against a `LDAP
|
|
281 <https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol>`__
|
|
282 or `OAuth2 <https://en.wikipedia.org/wiki/OAuth>`__ server.
|
|
283
|
|
284
|
|
285 .. _lua-auto-routing:
|
|
286
|
|
287 Auto-Routing of DICOM Images
|
|
288 ----------------------------
|
|
289
|
|
290 .. highlight:: lua
|
|
291
|
|
292 Since release 0.8.0, the routing of DICOM flows can be very easily
|
|
293 automated with Orthanc. All you have to do is to declare your
|
|
294 destination modality in the :ref:`configuration file <configuration>`
|
|
295 (section ``DicomModalities``), then to create and install a Lua
|
|
296 script. For instance, here is a sample script::
|
|
297
|
|
298 function OnStoredInstance(instanceId, tags, metadata)
|
|
299 Delete(SendToModality(instanceId, 'sample'))
|
|
300 end
|
|
301
|
|
302 If this script is loaded into Orthanc, whenever a new DICOM instance
|
|
303 is received by Orthanc, it will be routed to the modality whose
|
|
304 symbolic name is ``sample`` (through a Store-SCU command), then it
|
|
305 will be removed from Orthanc. In other words, this is a **one-liner
|
|
306 script to implement DICOM auto-routing**.
|
|
307
|
|
308 Very importantly, thanks to this feature, you do not have to use the
|
|
309 REST API or to create external scripts in order to automate simple
|
|
310 imaging flows. The scripting engine is entirely contained inside the
|
|
311 Orthanc core system.
|
|
312
|
|
313 Thanks to Lua expressiveness, you can also implement conditional
|
|
314 auto-routing. For instance, if you wish to route only patients whose
|
|
315 name contains "David", you would simply write::
|
|
316
|
|
317 function OnStoredInstance(instanceId, tags, metadata)
|
|
318 -- Extract the value of the "PatientName" DICOM tag
|
|
319 local patientName = string.lower(tags['PatientName'])
|
|
320
|
|
321 if string.find(patientName, 'david') ~= nil then
|
|
322 -- Only route patients whose name contains "David"
|
|
323 Delete(SendToModality(instanceId, 'sample'))
|
|
324
|
|
325 else
|
|
326 -- Delete the patients that are not called "David"
|
|
327 Delete(instanceId)
|
|
328 end
|
|
329 end
|
|
330
|
|
331 Besides ``SendToModality()``, a mostly identical function with the
|
|
332 same arguments called ``SendToPeer()`` can be used to route instances
|
|
333 to :ref:`Orthanc peers <peers>`. It is also possible to modify the
|
|
334 received instances before routing them. For instance, here is how you
|
|
335 would replace the ``StationName`` DICOM tag::
|
|
336
|
|
337 function OnStoredInstance(instanceId, tags, metadata)
|
|
338 -- Ignore the instances that result from a modification to avoid
|
|
339 -- infinite loops
|
|
340 if (metadata['ModifiedFrom'] == nil and
|
|
341 metadata['AnonymizedFrom'] == nil) then
|
|
342
|
|
343 -- The tags to be replaced
|
|
344 local replace = {}
|
|
345 replace['StationName'] = 'My Medical Device'
|
|
346
|
|
347 -- The tags to be removed
|
|
348 local remove = { 'MilitaryRank' }
|
|
349
|
|
350 -- Modify the instance, send it, then delete the modified instance
|
|
351 Delete(SendToModality(ModifyInstance(instanceId, replace, remove, true), 'sample'))
|
|
352
|
|
353 -- Delete the original instance
|
|
354 Delete(instanceId)
|
|
355 end
|
|
356 end
|
|
357
|
|
358
|
|
359 **Important remark:** The ``SendToModality()``, ``SendToPeer()``,
|
|
360 ``ModifyInstance()`` and ``Delete()`` functions are for the most
|
|
361 common cases of auto-routing (implying a single DICOM instance, and
|
|
362 possibly a basic modification of this instance). For more evolved
|
|
363 auto-routing scenarios, remember that Lua scripts :ref:`have full to
|
|
364 the REST API of Orthanc <lua-rest>`, and that :ref:`other callbacks
|
|
365 are available <lua-callbacks>` to react to other events than the
|
|
366 reception of a single instance (notably ``OnStablePatient()``,
|
|
367 ``OnStableStudy()`` and ``OnStableSeries()``).
|