2018-06-18 19:15:50 +03:00
|
|
|
<?php
|
|
|
|
/**
|
2019-07-29 16:39:43 +03:00
|
|
|
* @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
|
2018-06-18 19:15:50 +03:00
|
|
|
*
|
2020-04-29 12:57:22 +03:00
|
|
|
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
2018-06-18 19:15:50 +03:00
|
|
|
* @author Georg Ehrke <oc.list@georgehrke.com>
|
|
|
|
*
|
|
|
|
* @license GNU AGPL version 3 or any later version
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as
|
|
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
2019-12-03 21:57:53 +03:00
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2018-06-18 19:15:50 +03:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace OCA\DAV\BackgroundJob;
|
|
|
|
|
|
|
|
use OC\BackgroundJob\TimedJob;
|
|
|
|
use OCA\DAV\CalDAV\CalDavBackend;
|
|
|
|
use OCP\Calendar\BackendTemporarilyUnavailableException;
|
2019-07-29 16:39:43 +03:00
|
|
|
use OCP\Calendar\IMetadataProvider;
|
|
|
|
use OCP\Calendar\Resource\IBackend as IResourceBackend;
|
2018-06-18 19:15:50 +03:00
|
|
|
use OCP\Calendar\Resource\IManager as IResourceManager;
|
|
|
|
use OCP\Calendar\Resource\IResource;
|
|
|
|
use OCP\Calendar\Room\IManager as IRoomManager;
|
|
|
|
use OCP\Calendar\Room\IRoom;
|
|
|
|
use OCP\IDBConnection;
|
|
|
|
|
|
|
|
class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob {
|
|
|
|
|
|
|
|
/** @var IResourceManager */
|
|
|
|
private $resourceManager;
|
|
|
|
|
|
|
|
/** @var IRoomManager */
|
|
|
|
private $roomManager;
|
|
|
|
|
|
|
|
/** @var IDBConnection */
|
2019-07-29 16:39:43 +03:00
|
|
|
private $dbConnection;
|
2018-06-18 19:15:50 +03:00
|
|
|
|
|
|
|
/** @var CalDavBackend */
|
|
|
|
private $calDavBackend;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* UpdateCalendarResourcesRoomsBackgroundJob constructor.
|
|
|
|
*
|
|
|
|
* @param IResourceManager $resourceManager
|
|
|
|
* @param IRoomManager $roomManager
|
|
|
|
* @param IDBConnection $dbConnection
|
|
|
|
* @param CalDavBackend $calDavBackend
|
|
|
|
*/
|
2019-07-29 16:39:43 +03:00
|
|
|
public function __construct(IResourceManager $resourceManager,
|
|
|
|
IRoomManager $roomManager,
|
|
|
|
IDBConnection $dbConnection,
|
|
|
|
CalDavBackend $calDavBackend) {
|
2018-06-18 19:15:50 +03:00
|
|
|
$this->resourceManager = $resourceManager;
|
|
|
|
$this->roomManager = $roomManager;
|
2019-07-29 16:39:43 +03:00
|
|
|
$this->dbConnection = $dbConnection;
|
2018-06-18 19:15:50 +03:00
|
|
|
$this->calDavBackend = $calDavBackend;
|
|
|
|
|
|
|
|
// run once an hour
|
|
|
|
$this->setInterval(60 * 60);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $argument
|
|
|
|
*/
|
2019-07-29 16:39:43 +03:00
|
|
|
public function run($argument):void {
|
|
|
|
$this->runForBackend(
|
|
|
|
$this->resourceManager,
|
|
|
|
'calendar_resources',
|
|
|
|
'calendar_resources_md',
|
|
|
|
'resource_id',
|
|
|
|
'principals/calendar-resources'
|
|
|
|
);
|
|
|
|
$this->runForBackend(
|
|
|
|
$this->roomManager,
|
|
|
|
'calendar_rooms',
|
|
|
|
'calendar_rooms_md',
|
|
|
|
'room_id',
|
|
|
|
'principals/calendar-rooms'
|
|
|
|
);
|
2018-06-18 19:15:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-07-29 16:39:43 +03:00
|
|
|
* Run background-job for one specific backendManager
|
|
|
|
* either ResourceManager or RoomManager
|
|
|
|
*
|
|
|
|
* @param IResourceManager|IRoomManager $backendManager
|
|
|
|
* @param string $dbTable
|
|
|
|
* @param string $dbTableMetadata
|
|
|
|
* @param string $foreignKey
|
|
|
|
* @param string $principalPrefix
|
2018-06-18 19:15:50 +03:00
|
|
|
*/
|
2019-07-29 16:39:43 +03:00
|
|
|
private function runForBackend($backendManager,
|
|
|
|
string $dbTable,
|
|
|
|
string $dbTableMetadata,
|
|
|
|
string $foreignKey,
|
|
|
|
string $principalPrefix):void {
|
|
|
|
$backends = $backendManager->getBackends();
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($backends as $backend) {
|
2019-07-29 16:39:43 +03:00
|
|
|
$backendId = $backend->getBackendIdentifier();
|
2018-06-18 19:15:50 +03:00
|
|
|
|
|
|
|
try {
|
2019-07-29 16:39:43 +03:00
|
|
|
if ($backend instanceof IResourceBackend) {
|
|
|
|
$list = $backend->listAllResources();
|
|
|
|
} else {
|
|
|
|
$list = $backend->listAllRooms();
|
|
|
|
}
|
2020-04-10 15:19:56 +03:00
|
|
|
} catch (BackendTemporarilyUnavailableException $ex) {
|
2019-07-29 16:39:43 +03:00
|
|
|
continue;
|
2018-06-18 19:15:50 +03:00
|
|
|
}
|
|
|
|
|
2019-07-29 16:39:43 +03:00
|
|
|
$cachedList = $this->getAllCachedByBackend($dbTable, $backendId);
|
|
|
|
$newIds = array_diff($list, $cachedList);
|
|
|
|
$deletedIds = array_diff($cachedList, $list);
|
|
|
|
$editedIds = array_intersect($list, $cachedList);
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($newIds as $newId) {
|
2019-07-29 16:39:43 +03:00
|
|
|
try {
|
|
|
|
if ($backend instanceof IResourceBackend) {
|
|
|
|
$resource = $backend->getResource($newId);
|
|
|
|
} else {
|
|
|
|
$resource = $backend->getRoom($newId);
|
|
|
|
}
|
|
|
|
|
|
|
|
$metadata = [];
|
|
|
|
if ($resource instanceof IMetadataProvider) {
|
|
|
|
$metadata = $this->getAllMetadataOfBackend($resource);
|
|
|
|
}
|
2020-04-10 15:19:56 +03:00
|
|
|
} catch (BackendTemporarilyUnavailableException $ex) {
|
2018-08-24 16:21:04 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-07-29 16:39:43 +03:00
|
|
|
$id = $this->addToCache($dbTable, $backendId, $resource);
|
|
|
|
$this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata);
|
|
|
|
// we don't create the calendar here, it is created lazily
|
|
|
|
// when an event is actually scheduled with this resource / room
|
2018-06-18 19:15:50 +03:00
|
|
|
}
|
2018-08-24 16:21:04 +03:00
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($deletedIds as $deletedId) {
|
2019-07-29 16:39:43 +03:00
|
|
|
$id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId);
|
|
|
|
$this->deleteFromCache($dbTable, $id);
|
|
|
|
$this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id);
|
2018-06-18 19:15:50 +03:00
|
|
|
|
2019-07-29 16:39:43 +03:00
|
|
|
$principalName = implode('-', [$backendId, $deletedId]);
|
|
|
|
$this->deleteCalendarDataForResource($principalPrefix, $principalName);
|
2018-06-18 19:15:50 +03:00
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($editedIds as $editedId) {
|
2019-07-29 16:39:43 +03:00
|
|
|
$id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId);
|
|
|
|
|
|
|
|
try {
|
|
|
|
if ($backend instanceof IResourceBackend) {
|
|
|
|
$resource = $backend->getResource($editedId);
|
|
|
|
} else {
|
|
|
|
$resource = $backend->getRoom($editedId);
|
|
|
|
}
|
|
|
|
|
|
|
|
$metadata = [];
|
|
|
|
if ($resource instanceof IMetadataProvider) {
|
|
|
|
$metadata = $this->getAllMetadataOfBackend($resource);
|
|
|
|
}
|
2020-04-10 15:19:56 +03:00
|
|
|
} catch (BackendTemporarilyUnavailableException $ex) {
|
2018-08-24 16:21:04 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-07-29 16:39:43 +03:00
|
|
|
$this->updateCache($dbTable, $id, $resource);
|
2018-06-18 19:15:50 +03:00
|
|
|
|
2019-07-29 16:39:43 +03:00
|
|
|
if ($resource instanceof IMetadataProvider) {
|
|
|
|
$cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id);
|
|
|
|
$this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata);
|
|
|
|
}
|
2018-06-18 19:15:50 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* add entry to cache that exists remotely but not yet in cache
|
|
|
|
*
|
|
|
|
* @param string $table
|
2019-07-29 16:39:43 +03:00
|
|
|
* @param string $backendId
|
2018-06-18 19:15:50 +03:00
|
|
|
* @param IResource|IRoom $remote
|
2019-07-29 16:39:43 +03:00
|
|
|
* @return int Insert id
|
2018-06-18 19:15:50 +03:00
|
|
|
*/
|
2019-07-29 16:39:43 +03:00
|
|
|
private function addToCache(string $table,
|
|
|
|
string $backendId,
|
|
|
|
$remote):int {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
2018-06-18 19:15:50 +03:00
|
|
|
$query->insert($table)
|
|
|
|
->values([
|
2019-07-29 16:39:43 +03:00
|
|
|
'backend_id' => $query->createNamedParameter($backendId),
|
2018-06-18 19:15:50 +03:00
|
|
|
'resource_id' => $query->createNamedParameter($remote->getId()),
|
|
|
|
'email' => $query->createNamedParameter($remote->getEMail()),
|
|
|
|
'displayname' => $query->createNamedParameter($remote->getDisplayName()),
|
|
|
|
'group_restrictions' => $query->createNamedParameter(
|
|
|
|
$this->serializeGroupRestrictions(
|
|
|
|
$remote->getGroupRestrictions()
|
|
|
|
))
|
|
|
|
])
|
|
|
|
->execute();
|
2019-07-29 16:39:43 +03:00
|
|
|
return $query->getLastInsertId();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $table
|
|
|
|
* @param string $foreignKey
|
|
|
|
* @param int $foreignId
|
|
|
|
* @param array $metadata
|
|
|
|
*/
|
|
|
|
private function addMetadataToCache(string $table,
|
|
|
|
string $foreignKey,
|
|
|
|
int $foreignId,
|
|
|
|
array $metadata):void {
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($metadata as $key => $value) {
|
2019-07-29 16:39:43 +03:00
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
|
|
|
$query->insert($table)
|
|
|
|
->values([
|
|
|
|
$foreignKey => $query->createNamedParameter($foreignId),
|
|
|
|
'key' => $query->createNamedParameter($key),
|
|
|
|
'value' => $query->createNamedParameter($value),
|
|
|
|
])
|
|
|
|
->execute();
|
|
|
|
}
|
2018-06-18 19:15:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* delete entry from cache that does not exist anymore remotely
|
|
|
|
*
|
|
|
|
* @param string $table
|
2019-07-29 16:39:43 +03:00
|
|
|
* @param int $id
|
2018-06-18 19:15:50 +03:00
|
|
|
*/
|
2019-07-29 16:39:43 +03:00
|
|
|
private function deleteFromCache(string $table,
|
|
|
|
int $id):void {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
2018-06-18 19:15:50 +03:00
|
|
|
$query->delete($table)
|
2019-07-29 16:39:43 +03:00
|
|
|
->where($query->expr()->eq('id', $query->createNamedParameter($id)))
|
2018-06-18 19:15:50 +03:00
|
|
|
->execute();
|
2019-07-29 16:39:43 +03:00
|
|
|
}
|
2018-06-18 19:15:50 +03:00
|
|
|
|
2019-07-29 16:39:43 +03:00
|
|
|
/**
|
|
|
|
* @param string $table
|
|
|
|
* @param string $foreignKey
|
|
|
|
* @param int $id
|
|
|
|
*/
|
|
|
|
private function deleteMetadataFromCache(string $table,
|
|
|
|
string $foreignKey,
|
|
|
|
int $id):void {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
|
|
|
$query->delete($table)
|
|
|
|
->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
|
|
|
|
->execute();
|
2018-06-18 19:15:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* update an existing entry in cache
|
|
|
|
*
|
|
|
|
* @param string $table
|
2019-07-29 16:39:43 +03:00
|
|
|
* @param int $id
|
2018-06-18 19:15:50 +03:00
|
|
|
* @param IResource|IRoom $remote
|
|
|
|
*/
|
2019-07-29 16:39:43 +03:00
|
|
|
private function updateCache(string $table,
|
|
|
|
int $id,
|
|
|
|
$remote):void {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
2018-06-18 19:15:50 +03:00
|
|
|
$query->update($table)
|
|
|
|
->set('email', $query->createNamedParameter($remote->getEMail()))
|
|
|
|
->set('displayname', $query->createNamedParameter($remote->getDisplayName()))
|
|
|
|
->set('group_restrictions', $query->createNamedParameter(
|
|
|
|
$this->serializeGroupRestrictions(
|
|
|
|
$remote->getGroupRestrictions()
|
|
|
|
)))
|
2019-07-29 16:39:43 +03:00
|
|
|
->where($query->expr()->eq('id', $query->createNamedParameter($id)))
|
2018-06-18 19:15:50 +03:00
|
|
|
->execute();
|
|
|
|
}
|
|
|
|
|
2019-07-29 16:39:43 +03:00
|
|
|
/**
|
|
|
|
* @param string $dbTable
|
|
|
|
* @param string $foreignKey
|
|
|
|
* @param int $id
|
|
|
|
* @param array $metadata
|
|
|
|
* @param array $cachedMetadata
|
|
|
|
*/
|
|
|
|
private function updateMetadataCache(string $dbTable,
|
|
|
|
string $foreignKey,
|
|
|
|
int $id,
|
|
|
|
array $metadata,
|
|
|
|
array $cachedMetadata):void {
|
|
|
|
$newMetadata = array_diff_key($metadata, $cachedMetadata);
|
|
|
|
$deletedMetadata = array_diff_key($cachedMetadata, $metadata);
|
|
|
|
|
|
|
|
foreach ($newMetadata as $key => $value) {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
|
|
|
$query->insert($dbTable)
|
|
|
|
->values([
|
|
|
|
$foreignKey => $query->createNamedParameter($id),
|
|
|
|
'key' => $query->createNamedParameter($key),
|
|
|
|
'value' => $query->createNamedParameter($value),
|
|
|
|
])
|
|
|
|
->execute();
|
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($deletedMetadata as $key => $value) {
|
2019-07-29 16:39:43 +03:00
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
|
|
|
$query->delete($dbTable)
|
|
|
|
->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
|
|
|
|
->andWhere($query->expr()->eq('key', $query->createNamedParameter($key)))
|
|
|
|
->execute();
|
|
|
|
}
|
|
|
|
|
|
|
|
$existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata));
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($existingKeys as $existingKey) {
|
2019-07-29 16:39:43 +03:00
|
|
|
if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
|
|
|
$query->update($dbTable)
|
|
|
|
->set('value', $query->createNamedParameter($metadata[$existingKey]))
|
|
|
|
->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
|
|
|
|
->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey)))
|
|
|
|
->execute();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-18 19:15:50 +03:00
|
|
|
/**
|
|
|
|
* serialize array of group restrictions to store them in database
|
|
|
|
*
|
|
|
|
* @param array $groups
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
private function serializeGroupRestrictions(array $groups):string {
|
|
|
|
return \json_encode($groups);
|
|
|
|
}
|
2019-07-29 16:39:43 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets all metadata of a backend
|
|
|
|
*
|
|
|
|
* @param IResource|IRoom $resource
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function getAllMetadataOfBackend($resource):array {
|
|
|
|
if (!($resource instanceof IMetadataProvider)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$keys = $resource->getAllAvailableMetadataKeys();
|
|
|
|
$metadata = [];
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($keys as $key) {
|
2019-07-29 16:39:43 +03:00
|
|
|
$metadata[$key] = $resource->getMetadataForKey($key);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $metadata;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $table
|
|
|
|
* @param string $foreignKey
|
|
|
|
* @param int $id
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function getAllMetadataOfCache(string $table,
|
|
|
|
string $foreignKey,
|
|
|
|
int $id):array {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
|
|
|
$query->select(['key', 'value'])
|
|
|
|
->from($table)
|
|
|
|
->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)));
|
|
|
|
$stmt = $query->execute();
|
|
|
|
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
|
|
|
|
|
|
|
$metadata = [];
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($rows as $row) {
|
2019-07-29 16:39:43 +03:00
|
|
|
$metadata[$row['key']] = $row['value'];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $metadata;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets all cached rooms / resources by backend
|
|
|
|
*
|
|
|
|
* @param $tableName
|
|
|
|
* @param $backendId
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function getAllCachedByBackend(string $tableName,
|
|
|
|
string $backendId):array {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
|
|
|
$query->select('resource_id')
|
|
|
|
->from($tableName)
|
|
|
|
->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)));
|
|
|
|
$stmt = $query->execute();
|
|
|
|
|
2021-02-14 14:43:31 +03:00
|
|
|
return array_map(function ($row): string {
|
2019-07-29 16:39:43 +03:00
|
|
|
return $row['resource_id'];
|
2021-01-03 17:28:31 +03:00
|
|
|
}, $stmt->fetchAll());
|
2019-07-29 16:39:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $principalPrefix
|
|
|
|
* @param $principalUri
|
|
|
|
*/
|
|
|
|
private function deleteCalendarDataForResource(string $principalPrefix,
|
|
|
|
string $principalUri):void {
|
|
|
|
$calendar = $this->calDavBackend->getCalendarByUri(
|
|
|
|
implode('/', [$principalPrefix, $principalUri]),
|
|
|
|
CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI);
|
|
|
|
|
|
|
|
if ($calendar !== null) {
|
|
|
|
$this->calDavBackend->deleteCalendar($calendar['id']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $table
|
|
|
|
* @param $backendId
|
|
|
|
* @param $resourceId
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
private function getIdForBackendAndResource(string $table,
|
|
|
|
string $backendId,
|
|
|
|
string $resourceId):int {
|
|
|
|
$query = $this->dbConnection->getQueryBuilder();
|
|
|
|
$query->select('id')
|
|
|
|
->from($table)
|
|
|
|
->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
|
|
|
|
->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
|
|
|
|
$stmt = $query->execute();
|
|
|
|
|
2021-01-03 17:28:31 +03:00
|
|
|
return $stmt->fetch()['id'];
|
2019-07-29 16:39:43 +03:00
|
|
|
}
|
2018-06-18 19:15:50 +03:00
|
|
|
}
|