2011-10-11 17:41:25 +02:00
|
|
|
/**
|
2014-02-02 19:05:45 -06:00
|
|
|
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
2011-10-11 17:41:25 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package akka.event
|
|
|
|
|
|
|
|
|
|
import akka.actor.ActorRef
|
|
|
|
|
import akka.util.Index
|
|
|
|
|
import java.util.concurrent.ConcurrentSkipListSet
|
|
|
|
|
import java.util.Comparator
|
2011-11-10 00:26:53 +01:00
|
|
|
import akka.util.{ Subclassification, SubclassifiedIndex }
|
2012-03-15 15:45:55 +01:00
|
|
|
import scala.collection.immutable.TreeSet
|
2012-10-30 15:08:41 +01:00
|
|
|
import scala.collection.immutable
|
2011-10-11 17:41:25 +02:00
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Represents the base type for EventBuses
|
|
|
|
|
* Internally has an Event type, a Classifier type and a Subscriber type
|
|
|
|
|
*
|
2013-03-07 09:05:55 +01:00
|
|
|
* For the Java API, see akka.event.japi.*
|
2011-10-12 14:07:49 +02:00
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
trait EventBus {
|
|
|
|
|
type Event
|
|
|
|
|
type Classifier
|
|
|
|
|
type Subscriber
|
|
|
|
|
|
2014-02-06 15:08:51 +01:00
|
|
|
//#event-bus-api
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Attempts to register the subscriber to the specified Classifier
|
2014-02-06 15:08:51 +01:00
|
|
|
* @return true if successful and false if not (because it was already
|
|
|
|
|
* subscribed to that Classifier, or otherwise)
|
2011-10-12 14:07:49 +02:00
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
def subscribe(subscriber: Subscriber, to: Classifier): Boolean
|
2011-10-12 14:07:49 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Attempts to deregister the subscriber from the specified Classifier
|
2014-02-06 15:08:51 +01:00
|
|
|
* @return true if successful and false if not (because it wasn't subscribed
|
|
|
|
|
* to that Classifier, or otherwise)
|
2011-10-12 14:07:49 +02:00
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
def unsubscribe(subscriber: Subscriber, from: Classifier): Boolean
|
2011-10-12 14:07:49 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Attempts to deregister the subscriber from all Classifiers it may be subscribed to
|
|
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
def unsubscribe(subscriber: Subscriber): Unit
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Publishes the specified Event to this bus
|
|
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
def publish(event: Event): Unit
|
2014-02-06 15:08:51 +01:00
|
|
|
//#event-bus-api
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Represents an EventBus where the Subscriber type is ActorRef
|
|
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
trait ActorEventBus extends EventBus {
|
|
|
|
|
type Subscriber = ActorRef
|
2011-10-27 12:23:01 +02:00
|
|
|
protected def compareSubscribers(a: ActorRef, b: ActorRef) = a compareTo b
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Can be mixed into an EventBus to specify that the Classifier type is ActorRef
|
|
|
|
|
*/
|
2011-11-10 00:26:53 +01:00
|
|
|
trait ActorClassifier { this: EventBus ⇒
|
2011-10-11 17:41:25 +02:00
|
|
|
type Classifier = ActorRef
|
|
|
|
|
}
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Can be mixed into an EventBus to specify that the Classifier type is a Function from Event to Boolean (predicate)
|
|
|
|
|
*/
|
2011-11-10 00:26:53 +01:00
|
|
|
trait PredicateClassifier { this: EventBus ⇒
|
2011-10-11 17:41:25 +02:00
|
|
|
type Classifier = Event ⇒ Boolean
|
|
|
|
|
}
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Maps Subscribers to Classifiers using equality on Classifier to store a Set of Subscribers (hence the need for compareSubscribers)
|
|
|
|
|
* Maps Events to Classifiers through the classify-method (so it knows who to publish to)
|
|
|
|
|
*
|
|
|
|
|
* The compareSubscribers need to provide a total ordering of the Subscribers
|
|
|
|
|
*/
|
2011-11-10 00:26:53 +01:00
|
|
|
trait LookupClassification { this: EventBus ⇒
|
2011-10-12 11:46:49 +02:00
|
|
|
|
|
|
|
|
protected final val subscribers = new Index[Classifier, Subscriber](mapSize(), new Comparator[Subscriber] {
|
|
|
|
|
def compare(a: Subscriber, b: Subscriber): Int = compareSubscribers(a, b)
|
|
|
|
|
})
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* This is a size hint for the number of Classifiers you expect to have (use powers of 2)
|
|
|
|
|
*/
|
2011-10-12 11:46:49 +02:00
|
|
|
protected def mapSize(): Int
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Provides a total ordering of Subscribers (think java.util.Comparator.compare)
|
|
|
|
|
*/
|
2011-10-12 11:46:49 +02:00
|
|
|
protected def compareSubscribers(a: Subscriber, b: Subscriber): Int
|
2011-10-11 17:41:25 +02:00
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Returns the Classifier associated with the given Event
|
|
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
protected def classify(event: Event): Classifier
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Publishes the given Event to the given Subscriber
|
|
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
protected def publish(event: Event, subscriber: Subscriber): Unit
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
def subscribe(subscriber: Subscriber, to: Classifier): Boolean = subscribers.put(to, subscriber)
|
|
|
|
|
|
|
|
|
|
def unsubscribe(subscriber: Subscriber, from: Classifier): Boolean = subscribers.remove(from, subscriber)
|
|
|
|
|
|
|
|
|
|
def unsubscribe(subscriber: Subscriber): Unit = subscribers.removeValue(subscriber)
|
|
|
|
|
|
2011-10-12 11:46:49 +02:00
|
|
|
def publish(event: Event): Unit = {
|
|
|
|
|
val i = subscribers.valueIterator(classify(event))
|
|
|
|
|
while (i.hasNext) publish(event, i.next())
|
|
|
|
|
}
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
|
|
|
|
|
2011-11-10 00:26:53 +01:00
|
|
|
/**
|
|
|
|
|
* Classification which respects relationships between channels: subscribing
|
|
|
|
|
* to one channel automatically and idempotently subscribes to all sub-channels.
|
|
|
|
|
*/
|
|
|
|
|
trait SubchannelClassification { this: EventBus ⇒
|
|
|
|
|
|
2014-02-06 15:08:51 +01:00
|
|
|
/**
|
|
|
|
|
* The logic to form sub-class hierarchy
|
|
|
|
|
*/
|
2011-12-29 13:50:54 +01:00
|
|
|
protected implicit def subclassification: Subclassification[Classifier]
|
2011-11-10 00:26:53 +01:00
|
|
|
|
|
|
|
|
// must be lazy to avoid initialization order problem with subclassification
|
|
|
|
|
private lazy val subscriptions = new SubclassifiedIndex[Classifier, Subscriber]()
|
|
|
|
|
|
|
|
|
|
@volatile
|
|
|
|
|
private var cache = Map.empty[Classifier, Set[Subscriber]]
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the Classifier associated with the given Event
|
|
|
|
|
*/
|
|
|
|
|
protected def classify(event: Event): Classifier
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Publishes the given Event to the given Subscriber
|
|
|
|
|
*/
|
|
|
|
|
protected def publish(event: Event, subscriber: Subscriber): Unit
|
|
|
|
|
|
|
|
|
|
def subscribe(subscriber: Subscriber, to: Classifier): Boolean = subscriptions.synchronized {
|
|
|
|
|
val diff = subscriptions.addValue(to, subscriber)
|
2012-09-27 16:51:00 +02:00
|
|
|
addToCache(diff)
|
|
|
|
|
diff.nonEmpty
|
2011-11-10 00:26:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def unsubscribe(subscriber: Subscriber, from: Classifier): Boolean = subscriptions.synchronized {
|
|
|
|
|
val diff = subscriptions.removeValue(from, subscriber)
|
2012-09-28 15:18:05 +02:00
|
|
|
// removeValue(K, V) does not return the diff to remove from or add to the cache
|
|
|
|
|
// but instead the whole set of keys and values that should be updated in the cache
|
|
|
|
|
cache ++= diff
|
2012-09-27 16:51:00 +02:00
|
|
|
diff.nonEmpty
|
2011-11-10 00:26:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def unsubscribe(subscriber: Subscriber): Unit = subscriptions.synchronized {
|
2012-09-27 16:51:00 +02:00
|
|
|
removeFromCache(subscriptions.removeValue(subscriber))
|
2011-11-10 00:26:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def publish(event: Event): Unit = {
|
|
|
|
|
val c = classify(event)
|
2011-11-10 11:34:33 +01:00
|
|
|
val recv =
|
|
|
|
|
if (cache contains c) cache(c) // c will never be removed from cache
|
|
|
|
|
else subscriptions.synchronized {
|
|
|
|
|
if (cache contains c) cache(c)
|
|
|
|
|
else {
|
2012-09-27 13:15:31 +02:00
|
|
|
addToCache(subscriptions.addKey(c))
|
2011-11-10 00:26:53 +01:00
|
|
|
cache(c)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
recv foreach (publish(event, _))
|
|
|
|
|
}
|
2012-09-27 13:15:31 +02:00
|
|
|
|
2012-10-30 15:08:41 +01:00
|
|
|
private def removeFromCache(changes: immutable.Seq[(Classifier, Set[Subscriber])]): Unit =
|
2012-09-27 16:51:00 +02:00
|
|
|
cache = (cache /: changes) {
|
|
|
|
|
case (m, (c, cs)) ⇒ m.updated(c, m.getOrElse(c, Set.empty[Subscriber]) -- cs)
|
2012-09-27 13:15:31 +02:00
|
|
|
}
|
|
|
|
|
|
2012-10-30 15:08:41 +01:00
|
|
|
private def addToCache(changes: immutable.Seq[(Classifier, Set[Subscriber])]): Unit =
|
2012-09-27 13:15:31 +02:00
|
|
|
cache = (cache /: changes) {
|
|
|
|
|
case (m, (c, cs)) ⇒ m.updated(c, m.getOrElse(c, Set.empty[Subscriber]) ++ cs)
|
|
|
|
|
}
|
2011-11-10 00:26:53 +01:00
|
|
|
}
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Maps Classifiers to Subscribers and selects which Subscriber should receive which publication through scanning through all Subscribers
|
|
|
|
|
* through the matches(classifier, event) method
|
|
|
|
|
*
|
|
|
|
|
* Note: the compareClassifiers and compareSubscribers must together form an absolute ordering (think java.util.Comparator.compare)
|
|
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
trait ScanningClassification { self: EventBus ⇒
|
2011-10-12 11:46:49 +02:00
|
|
|
protected final val subscribers = new ConcurrentSkipListSet[(Classifier, Subscriber)](new Comparator[(Classifier, Subscriber)] {
|
2012-05-18 13:37:26 +02:00
|
|
|
def compare(a: (Classifier, Subscriber), b: (Classifier, Subscriber)): Int = compareClassifiers(a._1, b._1) match {
|
|
|
|
|
case 0 ⇒ compareSubscribers(a._2, b._2)
|
|
|
|
|
case other ⇒ other
|
2011-10-12 11:46:49 +02:00
|
|
|
}
|
|
|
|
|
})
|
2011-10-11 17:41:25 +02:00
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Provides a total ordering of Classifiers (think java.util.Comparator.compare)
|
|
|
|
|
*/
|
|
|
|
|
protected def compareClassifiers(a: Classifier, b: Classifier): Int
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Provides a total ordering of Subscribers (think java.util.Comparator.compare)
|
|
|
|
|
*/
|
|
|
|
|
protected def compareSubscribers(a: Subscriber, b: Subscriber): Int
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns whether the specified Classifier matches the specified Event
|
|
|
|
|
*/
|
|
|
|
|
protected def matches(classifier: Classifier, event: Event): Boolean
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Publishes the specified Event to the specified Subscriber
|
|
|
|
|
*/
|
|
|
|
|
protected def publish(event: Event, subscriber: Subscriber): Unit
|
|
|
|
|
|
2011-10-11 17:41:25 +02:00
|
|
|
def subscribe(subscriber: Subscriber, to: Classifier): Boolean = subscribers.add((to, subscriber))
|
2011-10-12 14:07:49 +02:00
|
|
|
|
2011-10-11 17:41:25 +02:00
|
|
|
def unsubscribe(subscriber: Subscriber, from: Classifier): Boolean = subscribers.remove((from, subscriber))
|
2011-10-12 14:07:49 +02:00
|
|
|
|
2011-10-11 17:41:25 +02:00
|
|
|
def unsubscribe(subscriber: Subscriber): Unit = {
|
|
|
|
|
val i = subscribers.iterator()
|
|
|
|
|
while (i.hasNext) {
|
|
|
|
|
val e = i.next()
|
2011-10-12 11:46:49 +02:00
|
|
|
if (compareSubscribers(subscriber, e._2) == 0) i.remove()
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def publish(event: Event): Unit = {
|
|
|
|
|
val currentSubscribers = subscribers.iterator()
|
|
|
|
|
while (currentSubscribers.hasNext) {
|
|
|
|
|
val (classifier, subscriber) = currentSubscribers.next()
|
2011-10-12 11:46:49 +02:00
|
|
|
if (matches(classifier, event))
|
|
|
|
|
publish(event, subscriber)
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Maps ActorRefs to ActorRefs to form an EventBus where ActorRefs can listen to other ActorRefs
|
|
|
|
|
*/
|
2011-11-10 00:26:53 +01:00
|
|
|
trait ActorClassification { this: ActorEventBus with ActorClassifier ⇒
|
2011-10-11 17:41:25 +02:00
|
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
|
|
|
import scala.annotation.tailrec
|
2012-03-15 15:17:08 +01:00
|
|
|
private val empty = TreeSet.empty[ActorRef]
|
2012-05-18 13:37:26 +02:00
|
|
|
private val mappings = new ConcurrentHashMap[ActorRef, TreeSet[ActorRef]](mapSize)
|
2011-10-11 17:41:25 +02:00
|
|
|
|
|
|
|
|
@tailrec
|
|
|
|
|
protected final def associate(monitored: ActorRef, monitor: ActorRef): Boolean = {
|
|
|
|
|
val current = mappings get monitored
|
|
|
|
|
current match {
|
|
|
|
|
case null ⇒
|
2012-05-03 21:14:47 +02:00
|
|
|
if (monitored.isTerminated) false
|
2011-10-11 17:41:25 +02:00
|
|
|
else {
|
2012-03-15 15:45:55 +01:00
|
|
|
if (mappings.putIfAbsent(monitored, empty + monitor) ne null) associate(monitored, monitor)
|
2012-05-03 21:14:47 +02:00
|
|
|
else if (monitored.isTerminated) !dissociate(monitored, monitor) else true
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
2012-03-15 15:17:08 +01:00
|
|
|
case raw: TreeSet[_] ⇒
|
|
|
|
|
val v = raw.asInstanceOf[TreeSet[ActorRef]]
|
2012-05-03 21:14:47 +02:00
|
|
|
if (monitored.isTerminated) false
|
2011-10-11 17:41:25 +02:00
|
|
|
if (v.contains(monitor)) true
|
|
|
|
|
else {
|
2012-03-15 15:17:08 +01:00
|
|
|
val added = v + monitor
|
2011-10-11 17:41:25 +02:00
|
|
|
if (!mappings.replace(monitored, v, added)) associate(monitored, monitor)
|
2012-05-03 21:14:47 +02:00
|
|
|
else if (monitored.isTerminated) !dissociate(monitored, monitor) else true
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-11-07 16:35:14 +01:00
|
|
|
protected final def dissociate(monitored: ActorRef): immutable.Iterable[ActorRef] = {
|
2011-10-11 17:41:25 +02:00
|
|
|
@tailrec
|
2012-11-07 16:35:14 +01:00
|
|
|
def dissociateAsMonitored(monitored: ActorRef): immutable.Iterable[ActorRef] = {
|
2011-10-11 17:41:25 +02:00
|
|
|
val current = mappings get monitored
|
|
|
|
|
current match {
|
2012-03-15 15:17:08 +01:00
|
|
|
case null ⇒ empty
|
|
|
|
|
case raw: TreeSet[_] ⇒
|
|
|
|
|
val v = raw.asInstanceOf[TreeSet[ActorRef]]
|
2011-10-11 17:41:25 +02:00
|
|
|
if (!mappings.remove(monitored, v)) dissociateAsMonitored(monitored)
|
|
|
|
|
else v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def dissociateAsMonitor(monitor: ActorRef): Unit = {
|
|
|
|
|
val i = mappings.entrySet.iterator
|
|
|
|
|
while (i.hasNext()) {
|
|
|
|
|
val entry = i.next()
|
|
|
|
|
val v = entry.getValue
|
|
|
|
|
v match {
|
2012-03-15 15:17:08 +01:00
|
|
|
case raw: TreeSet[_] ⇒
|
|
|
|
|
val monitors = raw.asInstanceOf[TreeSet[ActorRef]]
|
2011-10-11 17:41:25 +02:00
|
|
|
if (monitors.contains(monitor))
|
|
|
|
|
dissociate(entry.getKey, monitor)
|
|
|
|
|
case _ ⇒ //Dun care
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try { dissociateAsMonitored(monitored) } finally { dissociateAsMonitor(monitored) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@tailrec
|
|
|
|
|
protected final def dissociate(monitored: ActorRef, monitor: ActorRef): Boolean = {
|
|
|
|
|
val current = mappings get monitored
|
|
|
|
|
current match {
|
|
|
|
|
case null ⇒ false
|
2012-03-15 15:17:08 +01:00
|
|
|
case raw: TreeSet[_] ⇒
|
|
|
|
|
val v = raw.asInstanceOf[TreeSet[ActorRef]]
|
2012-03-16 16:29:11 +01:00
|
|
|
val removed = v - monitor
|
2011-10-19 14:26:53 +02:00
|
|
|
if (removed eq raw) false
|
2011-10-11 17:41:25 +02:00
|
|
|
else if (removed.isEmpty) {
|
2011-10-19 14:26:53 +02:00
|
|
|
if (!mappings.remove(monitored, v)) dissociate(monitored, monitor) else true
|
2011-10-11 17:41:25 +02:00
|
|
|
} else {
|
2011-10-19 14:26:53 +02:00
|
|
|
if (!mappings.replace(monitored, v, removed)) dissociate(monitored, monitor) else true
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* Returns the Classifier associated with the specified Event
|
|
|
|
|
*/
|
2011-10-11 17:41:25 +02:00
|
|
|
protected def classify(event: Event): Classifier
|
|
|
|
|
|
2011-10-12 14:07:49 +02:00
|
|
|
/**
|
|
|
|
|
* This is a size hint for the number of Classifiers you expect to have (use powers of 2)
|
|
|
|
|
*/
|
|
|
|
|
protected def mapSize: Int
|
|
|
|
|
|
2012-05-18 13:37:26 +02:00
|
|
|
def publish(event: Event): Unit = mappings.get(classify(event)) match {
|
|
|
|
|
case null ⇒ ()
|
|
|
|
|
case some ⇒ some foreach { _ ! event }
|
2011-10-11 17:41:25 +02:00
|
|
|
}
|
|
|
|
|
|
2012-06-19 11:39:05 +02:00
|
|
|
def subscribe(subscriber: Subscriber, to: Classifier): Boolean =
|
|
|
|
|
if (subscriber eq null) throw new IllegalArgumentException("Subscriber is null")
|
|
|
|
|
else if (to eq null) throw new IllegalArgumentException("Classifier is null")
|
|
|
|
|
else associate(to, subscriber)
|
|
|
|
|
|
|
|
|
|
def unsubscribe(subscriber: Subscriber, from: Classifier): Boolean =
|
|
|
|
|
if (subscriber eq null) throw new IllegalArgumentException("Subscriber is null")
|
|
|
|
|
else if (from eq null) throw new IllegalArgumentException("Classifier is null")
|
|
|
|
|
else dissociate(from, subscriber)
|
|
|
|
|
|
|
|
|
|
def unsubscribe(subscriber: Subscriber): Unit =
|
|
|
|
|
if (subscriber eq null) throw new IllegalArgumentException("Subscriber is null")
|
|
|
|
|
else dissociate(subscriber)
|
2011-10-28 15:55:15 +02:00
|
|
|
}
|