blob: f0e5ae46819b0a8ebec92018e2fb157486738be3 [file] [log] [blame] [view]
Roman Elizarov660c2d72020-02-14 13:18:37 +03001<!--- TEST_NAME SelectGuideTest -->
hadihariri7db55532018-09-15 10:35:08 +02002
Prendotab8a559d2018-11-30 16:24:23 +03003**Table of contents**
hadihariri7db55532018-09-15 10:35:08 +02004
5<!--- TOC -->
6
Roman Elizarov3258e1f2019-08-22 20:08:48 +03007* [Select Expression (experimental)](#select-expression-experimental)
hadihariri7db55532018-09-15 10:35:08 +02008 * [Selecting from channels](#selecting-from-channels)
9 * [Selecting on close](#selecting-on-close)
10 * [Selecting to send](#selecting-to-send)
11 * [Selecting deferred values](#selecting-deferred-values)
12 * [Switch over a channel of deferred values](#switch-over-a-channel-of-deferred-values)
13
Roman Elizarov660c2d72020-02-14 13:18:37 +030014<!--- END -->
hadihariri7db55532018-09-15 10:35:08 +020015
Roman Elizarov3258e1f2019-08-22 20:08:48 +030016## Select Expression (experimental)
hadihariri7db55532018-09-15 10:35:08 +020017
18Select expression makes it possible to await multiple suspending functions simultaneously and _select_
19the first one that becomes available.
20
21> Select expressions are an experimental feature of `kotlinx.coroutines`. Their API is expected to
22evolve in the upcoming updates of the `kotlinx.coroutines` library with potentially
23breaking changes.
24
25### Selecting from channels
26
27Let us have two producers of strings: `fizz` and `buzz`. The `fizz` produces "Fizz" string every 300 ms:
28
Alexander Prendotacbeef102018-09-27 18:42:04 +030029<div class="sample" markdown="1" theme="idea" data-highlight-only>
30
hadihariri7db55532018-09-15 10:35:08 +020031```kotlin
32fun CoroutineScope.fizz() = produce<String> {
33 while (true) { // sends "Fizz" every 300 ms
34 delay(300)
35 send("Fizz")
36 }
37}
38```
39
Alexander Prendotacbeef102018-09-27 18:42:04 +030040</div>
41
hadihariri7db55532018-09-15 10:35:08 +020042And the `buzz` produces "Buzz!" string every 500 ms:
43
Alexander Prendotacbeef102018-09-27 18:42:04 +030044<div class="sample" markdown="1" theme="idea" data-highlight-only>
45
hadihariri7db55532018-09-15 10:35:08 +020046```kotlin
47fun CoroutineScope.buzz() = produce<String> {
48 while (true) { // sends "Buzz!" every 500 ms
49 delay(500)
50 send("Buzz!")
51 }
52}
53```
54
Alexander Prendotacbeef102018-09-27 18:42:04 +030055</div>
56
hadihariri7db55532018-09-15 10:35:08 +020057Using [receive][ReceiveChannel.receive] suspending function we can receive _either_ from one channel or the
58other. But [select] expression allows us to receive from _both_ simultaneously using its
59[onReceive][ReceiveChannel.onReceive] clauses:
60
Alexander Prendotacbeef102018-09-27 18:42:04 +030061<div class="sample" markdown="1" theme="idea" data-highlight-only>
62
hadihariri7db55532018-09-15 10:35:08 +020063```kotlin
64suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
65 select<Unit> { // <Unit> means that this select expression does not produce any result
66 fizz.onReceive { value -> // this is the first select clause
67 println("fizz -> '$value'")
68 }
69 buzz.onReceive { value -> // this is the second select clause
70 println("buzz -> '$value'")
71 }
72 }
73}
74```
75
Alexander Prendotacbeef102018-09-27 18:42:04 +030076</div>
77
hadihariri7db55532018-09-15 10:35:08 +020078Let us run it all seven times:
79
Prendota0eee3c32018-10-22 12:52:56 +030080<!--- CLEAR -->
81
82<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +030083
hadihariri7db55532018-09-15 10:35:08 +020084```kotlin
Prendota0eee3c32018-10-22 12:52:56 +030085import kotlinx.coroutines.*
86import kotlinx.coroutines.channels.*
87import kotlinx.coroutines.selects.*
88
89fun CoroutineScope.fizz() = produce<String> {
90 while (true) { // sends "Fizz" every 300 ms
91 delay(300)
92 send("Fizz")
93 }
94}
95
96fun CoroutineScope.buzz() = produce<String> {
97 while (true) { // sends "Buzz!" every 500 ms
98 delay(500)
99 send("Buzz!")
100 }
101}
102
103suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
104 select<Unit> { // <Unit> means that this select expression does not produce any result
105 fizz.onReceive { value -> // this is the first select clause
106 println("fizz -> '$value'")
107 }
108 buzz.onReceive { value -> // this is the second select clause
109 println("buzz -> '$value'")
110 }
111 }
112}
113
Prendota65e6c8c2018-10-17 11:51:08 +0300114fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300115//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200116 val fizz = fizz()
117 val buzz = buzz()
118 repeat(7) {
119 selectFizzBuzz(fizz, buzz)
120 }
Prendota0eee3c32018-10-22 12:52:56 +0300121 coroutineContext.cancelChildren() // cancel fizz & buzz coroutines
122//sampleEnd
hadihariri7db55532018-09-15 10:35:08 +0200123}
124```
125
Alexander Prendotacbeef102018-09-27 18:42:04 +0300126</div>
127
Adam Howardf13549a2020-06-02 11:17:46 +0100128> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-01.kt).
hadihariri7db55532018-09-15 10:35:08 +0200129
130The result of this code is:
131
132```text
133fizz -> 'Fizz'
134buzz -> 'Buzz!'
135fizz -> 'Fizz'
136fizz -> 'Fizz'
137buzz -> 'Buzz!'
138fizz -> 'Fizz'
139buzz -> 'Buzz!'
140```
141
142<!--- TEST -->
143
144### Selecting on close
145
146The [onReceive][ReceiveChannel.onReceive] clause in `select` fails when the channel is closed causing the corresponding
Vsevolod Tolstopyatova8904e22019-07-17 17:22:05 -0700147`select` to throw an exception. We can use [onReceiveOrNull][onReceiveOrNull] clause to perform a
hadihariri7db55532018-09-15 10:35:08 +0200148specific action when the channel is closed. The following example also shows that `select` is an expression that returns
149the result of its selected clause:
150
Alexander Prendotacbeef102018-09-27 18:42:04 +0300151<div class="sample" markdown="1" theme="idea" data-highlight-only>
152
hadihariri7db55532018-09-15 10:35:08 +0200153```kotlin
154suspend fun selectAorB(a: ReceiveChannel<String>, b: ReceiveChannel<String>): String =
155 select<String> {
156 a.onReceiveOrNull { value ->
157 if (value == null)
158 "Channel 'a' is closed"
159 else
160 "a -> '$value'"
161 }
162 b.onReceiveOrNull { value ->
163 if (value == null)
164 "Channel 'b' is closed"
165 else
166 "b -> '$value'"
167 }
168 }
169```
170
Alexander Prendotacbeef102018-09-27 18:42:04 +0300171</div>
172
Vsevolod Tolstopyatova8904e22019-07-17 17:22:05 -0700173Note that [onReceiveOrNull][onReceiveOrNull] is an extension function defined only
174for channels with non-nullable elements so that there is no accidental confusion between a closed channel
175and a null value.
176
hadihariri7db55532018-09-15 10:35:08 +0200177Let's use it with channel `a` that produces "Hello" string four times and
178channel `b` that produces "World" four times:
179
Prendota0eee3c32018-10-22 12:52:56 +0300180<!--- CLEAR -->
181
182<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300183
hadihariri7db55532018-09-15 10:35:08 +0200184```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300185import kotlinx.coroutines.*
186import kotlinx.coroutines.channels.*
187import kotlinx.coroutines.selects.*
188
189suspend fun selectAorB(a: ReceiveChannel<String>, b: ReceiveChannel<String>): String =
190 select<String> {
191 a.onReceiveOrNull { value ->
192 if (value == null)
193 "Channel 'a' is closed"
194 else
195 "a -> '$value'"
196 }
197 b.onReceiveOrNull { value ->
198 if (value == null)
199 "Channel 'b' is closed"
200 else
201 "b -> '$value'"
202 }
203 }
204
Prendota65e6c8c2018-10-17 11:51:08 +0300205fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300206//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200207 val a = produce<String> {
208 repeat(4) { send("Hello $it") }
209 }
210 val b = produce<String> {
211 repeat(4) { send("World $it") }
212 }
213 repeat(8) { // print first eight results
214 println(selectAorB(a, b))
215 }
Prendota0eee3c32018-10-22 12:52:56 +0300216 coroutineContext.cancelChildren()
217//sampleEnd
218}
hadihariri7db55532018-09-15 10:35:08 +0200219```
220
Alexander Prendotacbeef102018-09-27 18:42:04 +0300221</div>
222
Adam Howardf13549a2020-06-02 11:17:46 +0100223> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-02.kt).
hadihariri7db55532018-09-15 10:35:08 +0200224
225The result of this code is quite interesting, so we'll analyze it in mode detail:
226
227```text
228a -> 'Hello 0'
229a -> 'Hello 1'
230b -> 'World 0'
231a -> 'Hello 2'
232a -> 'Hello 3'
233b -> 'World 1'
234Channel 'a' is closed
235Channel 'a' is closed
236```
237
238<!--- TEST -->
239
240There are couple of observations to make out of it.
241
242First of all, `select` is _biased_ to the first clause. When several clauses are selectable at the same time,
243the first one among them gets selected. Here, both channels are constantly producing strings, so `a` channel,
244being the first clause in select, wins. However, because we are using unbuffered channel, the `a` gets suspended from
245time to time on its [send][SendChannel.send] invocation and gives a chance for `b` to send, too.
246
Vsevolod Tolstopyatova8904e22019-07-17 17:22:05 -0700247The second observation, is that [onReceiveOrNull][onReceiveOrNull] gets immediately selected when the
hadihariri7db55532018-09-15 10:35:08 +0200248channel is already closed.
249
250### Selecting to send
251
252Select expression has [onSend][SendChannel.onSend] clause that can be used for a great good in combination
253with a biased nature of selection.
254
255Let us write an example of producer of integers that sends its values to a `side` channel when
256the consumers on its primary channel cannot keep up with it:
257
Alexander Prendotacbeef102018-09-27 18:42:04 +0300258<div class="sample" markdown="1" theme="idea" data-highlight-only>
259
hadihariri7db55532018-09-15 10:35:08 +0200260```kotlin
261fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
262 for (num in 1..10) { // produce 10 numbers from 1 to 10
263 delay(100) // every 100 ms
264 select<Unit> {
265 onSend(num) {} // Send to the primary channel
266 side.onSend(num) {} // or to the side channel
267 }
268 }
269}
270```
271
Alexander Prendotacbeef102018-09-27 18:42:04 +0300272</div>
273
hadihariri7db55532018-09-15 10:35:08 +0200274Consumer is going to be quite slow, taking 250 ms to process each number:
275
Prendota0eee3c32018-10-22 12:52:56 +0300276<!--- CLEAR -->
277
278<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300279
hadihariri7db55532018-09-15 10:35:08 +0200280```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300281import kotlinx.coroutines.*
282import kotlinx.coroutines.channels.*
283import kotlinx.coroutines.selects.*
284
285fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
286 for (num in 1..10) { // produce 10 numbers from 1 to 10
287 delay(100) // every 100 ms
288 select<Unit> {
289 onSend(num) {} // Send to the primary channel
290 side.onSend(num) {} // or to the side channel
291 }
292 }
293}
294
Prendota65e6c8c2018-10-17 11:51:08 +0300295fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300296//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200297 val side = Channel<Int>() // allocate side channel
298 launch { // this is a very fast consumer for the side channel
299 side.consumeEach { println("Side channel has $it") }
300 }
301 produceNumbers(side).consumeEach {
302 println("Consuming $it")
303 delay(250) // let us digest the consumed number properly, do not hurry
304 }
305 println("Done consuming")
Prendota0eee3c32018-10-22 12:52:56 +0300306 coroutineContext.cancelChildren()
307//sampleEnd
hadihariri7db55532018-09-15 10:35:08 +0200308}
Alexander Prendotacbeef102018-09-27 18:42:04 +0300309```
310
311</div>
hadihariri7db55532018-09-15 10:35:08 +0200312
Adam Howardf13549a2020-06-02 11:17:46 +0100313> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-03.kt).
hadihariri7db55532018-09-15 10:35:08 +0200314
315So let us see what happens:
316
317```text
318Consuming 1
319Side channel has 2
320Side channel has 3
321Consuming 4
322Side channel has 5
323Side channel has 6
324Consuming 7
325Side channel has 8
326Side channel has 9
327Consuming 10
328Done consuming
329```
330
331<!--- TEST -->
332
333### Selecting deferred values
334
335Deferred values can be selected using [onAwait][Deferred.onAwait] clause.
336Let us start with an async function that returns a deferred string value after
337a random delay:
338
Alexander Prendotacbeef102018-09-27 18:42:04 +0300339<div class="sample" markdown="1" theme="idea" data-highlight-only>
340
hadihariri7db55532018-09-15 10:35:08 +0200341```kotlin
342fun CoroutineScope.asyncString(time: Int) = async {
343 delay(time.toLong())
344 "Waited for $time ms"
345}
346```
347
Alexander Prendotacbeef102018-09-27 18:42:04 +0300348</div>
349
hadihariri7db55532018-09-15 10:35:08 +0200350Let us start a dozen of them with a random delay.
351
Alexander Prendotacbeef102018-09-27 18:42:04 +0300352<div class="sample" markdown="1" theme="idea" data-highlight-only>
353
hadihariri7db55532018-09-15 10:35:08 +0200354```kotlin
355fun CoroutineScope.asyncStringsList(): List<Deferred<String>> {
356 val random = Random(3)
357 return List(12) { asyncString(random.nextInt(1000)) }
358}
359```
360
Alexander Prendotacbeef102018-09-27 18:42:04 +0300361</div>
362
hadihariri7db55532018-09-15 10:35:08 +0200363Now the main function awaits for the first of them to complete and counts the number of deferred values
Inegoebe519a2019-04-21 13:22:27 +0700364that are still active. Note that we've used here the fact that `select` expression is a Kotlin DSL,
hadihariri7db55532018-09-15 10:35:08 +0200365so we can provide clauses for it using an arbitrary code. In this case we iterate over a list
366of deferred values to provide `onAwait` clause for each deferred value.
367
Prendota0eee3c32018-10-22 12:52:56 +0300368<!--- CLEAR -->
369
370<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300371
hadihariri7db55532018-09-15 10:35:08 +0200372```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300373import kotlinx.coroutines.*
374import kotlinx.coroutines.selects.*
375import java.util.*
376
377fun CoroutineScope.asyncString(time: Int) = async {
378 delay(time.toLong())
379 "Waited for $time ms"
380}
381
382fun CoroutineScope.asyncStringsList(): List<Deferred<String>> {
383 val random = Random(3)
384 return List(12) { asyncString(random.nextInt(1000)) }
385}
386
Prendota65e6c8c2018-10-17 11:51:08 +0300387fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300388//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200389 val list = asyncStringsList()
390 val result = select<String> {
391 list.withIndex().forEach { (index, deferred) ->
392 deferred.onAwait { answer ->
393 "Deferred $index produced answer '$answer'"
394 }
395 }
396 }
397 println(result)
398 val countActive = list.count { it.isActive }
399 println("$countActive coroutines are still active")
Prendota0eee3c32018-10-22 12:52:56 +0300400//sampleEnd
hadihariri7db55532018-09-15 10:35:08 +0200401}
402```
403
Alexander Prendotacbeef102018-09-27 18:42:04 +0300404</div>
405
Adam Howardf13549a2020-06-02 11:17:46 +0100406> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-04.kt).
hadihariri7db55532018-09-15 10:35:08 +0200407
408The output is:
409
410```text
411Deferred 4 produced answer 'Waited for 128 ms'
41211 coroutines are still active
413```
414
415<!--- TEST -->
416
417### Switch over a channel of deferred values
418
419Let us write a channel producer function that consumes a channel of deferred string values, waits for each received
420deferred value, but only until the next deferred value comes over or the channel is closed. This example puts together
Vsevolod Tolstopyatova8904e22019-07-17 17:22:05 -0700421[onReceiveOrNull][onReceiveOrNull] and [onAwait][Deferred.onAwait] clauses in the same `select`:
hadihariri7db55532018-09-15 10:35:08 +0200422
Alexander Prendotacbeef102018-09-27 18:42:04 +0300423<div class="sample" markdown="1" theme="idea" data-highlight-only>
424
hadihariri7db55532018-09-15 10:35:08 +0200425```kotlin
426fun CoroutineScope.switchMapDeferreds(input: ReceiveChannel<Deferred<String>>) = produce<String> {
427 var current = input.receive() // start with first received deferred value
428 while (isActive) { // loop while not cancelled/closed
429 val next = select<Deferred<String>?> { // return next deferred value from this select or null
430 input.onReceiveOrNull { update ->
431 update // replaces next value to wait
432 }
433 current.onAwait { value ->
434 send(value) // send value that current deferred has produced
435 input.receiveOrNull() // and use the next deferred from the input channel
436 }
437 }
438 if (next == null) {
439 println("Channel was closed")
440 break // out of loop
441 } else {
442 current = next
443 }
444 }
445}
446```
447
Alexander Prendotacbeef102018-09-27 18:42:04 +0300448</div>
449
hadihariri7db55532018-09-15 10:35:08 +0200450To test it, we'll use a simple async function that resolves to a specified string after a specified time:
451
Alexander Prendotacbeef102018-09-27 18:42:04 +0300452
453<div class="sample" markdown="1" theme="idea" data-highlight-only>
454
hadihariri7db55532018-09-15 10:35:08 +0200455```kotlin
456fun CoroutineScope.asyncString(str: String, time: Long) = async {
457 delay(time)
458 str
459}
460```
461
Alexander Prendotacbeef102018-09-27 18:42:04 +0300462</div>
463
hadihariri7db55532018-09-15 10:35:08 +0200464The main function just launches a coroutine to print results of `switchMapDeferreds` and sends some test
465data to it:
466
Prendota0eee3c32018-10-22 12:52:56 +0300467<!--- CLEAR -->
468
469<div class="sample" markdown="1" theme="idea" data-min-compiler-version="1.3">
Alexander Prendotacbeef102018-09-27 18:42:04 +0300470
hadihariri7db55532018-09-15 10:35:08 +0200471```kotlin
Prendota0eee3c32018-10-22 12:52:56 +0300472import kotlinx.coroutines.*
473import kotlinx.coroutines.channels.*
474import kotlinx.coroutines.selects.*
475
476fun CoroutineScope.switchMapDeferreds(input: ReceiveChannel<Deferred<String>>) = produce<String> {
477 var current = input.receive() // start with first received deferred value
478 while (isActive) { // loop while not cancelled/closed
479 val next = select<Deferred<String>?> { // return next deferred value from this select or null
480 input.onReceiveOrNull { update ->
481 update // replaces next value to wait
482 }
483 current.onAwait { value ->
484 send(value) // send value that current deferred has produced
485 input.receiveOrNull() // and use the next deferred from the input channel
486 }
487 }
488 if (next == null) {
489 println("Channel was closed")
490 break // out of loop
491 } else {
492 current = next
493 }
494 }
495}
496
497fun CoroutineScope.asyncString(str: String, time: Long) = async {
498 delay(time)
499 str
500}
501
Prendota65e6c8c2018-10-17 11:51:08 +0300502fun main() = runBlocking<Unit> {
Prendota0eee3c32018-10-22 12:52:56 +0300503//sampleStart
hadihariri7db55532018-09-15 10:35:08 +0200504 val chan = Channel<Deferred<String>>() // the channel for test
505 launch { // launch printing coroutine
506 for (s in switchMapDeferreds(chan))
507 println(s) // print each received string
508 }
509 chan.send(asyncString("BEGIN", 100))
510 delay(200) // enough time for "BEGIN" to be produced
511 chan.send(asyncString("Slow", 500))
512 delay(100) // not enough time to produce slow
513 chan.send(asyncString("Replace", 100))
514 delay(500) // give it time before the last one
515 chan.send(asyncString("END", 500))
516 delay(1000) // give it time to process
517 chan.close() // close the channel ...
518 delay(500) // and wait some time to let it finish
Prendota0eee3c32018-10-22 12:52:56 +0300519//sampleEnd
hadihariri7db55532018-09-15 10:35:08 +0200520}
521```
522
Alexander Prendotacbeef102018-09-27 18:42:04 +0300523</div>
524
Adam Howardf13549a2020-06-02 11:17:46 +0100525> You can get the full code [here](../kotlinx-coroutines-core/jvm/test/guide/example-select-05.kt).
hadihariri7db55532018-09-15 10:35:08 +0200526
527The result of this code:
528
529```text
530BEGIN
531Replace
532END
533Channel was closed
534```
535
536<!--- TEST -->
537
538<!--- MODULE kotlinx-coroutines-core -->
Roman Elizarov0950dfa2018-07-13 10:33:25 +0300539<!--- INDEX kotlinx.coroutines -->
540[Deferred.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/on-await.html
541<!--- INDEX kotlinx.coroutines.channels -->
542[ReceiveChannel.receive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html
543[ReceiveChannel.onReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/on-receive.html
Vsevolod Tolstopyatova8904e22019-07-17 17:22:05 -0700544[onReceiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/on-receive-or-null.html
Roman Elizarov0950dfa2018-07-13 10:33:25 +0300545[SendChannel.send]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html
546[SendChannel.onSend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/on-send.html
547<!--- INDEX kotlinx.coroutines.selects -->
548[select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.selects/select.html
hadihariri7db55532018-09-15 10:35:08 +0200549<!--- END -->