Passed
Push — master ( 2adfe5...ed0bae )
by Pauli
04:04
created

BusinessLayer::maxId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 2
rs 10
1
<?php declare(strict_types=1);
2
/**
3
 * ownCloud
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Alessandro Cosentino <[email protected]>
9
 * @author Bernhard Posselt <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Alessandro Cosentino 2012
12
 * @copyright Bernhard Posselt 2012, 2014
13
 * @copyright Pauli Järvinen 2017 - 2024
14
 */
15
16
namespace OCA\Music\AppFramework\BusinessLayer;
17
18
use OCA\Music\Db\BaseMapper;
19
use OCA\Music\Db\Entity;
20
use OCA\Music\Db\MatchMode;
21
use OCA\Music\Db\SortBy;
22
use OCA\Music\Utility\Util;
23
use OCA\Music\Utility\Random;
24
25
use OCP\AppFramework\Db\DoesNotExistException;
26
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
27
use OCP\IL10N;
28
29
/**
30
 * @phpstan-template EntityType of Entity
31
 */
32
abstract class BusinessLayer {
33
	protected $mapper;
34
35
	// Some SQLite installations can't handle more than 999 query args. Remember that `user_id` takes one slot in most queries.
36
	public const MAX_SQL_ARGS = 999;
37
38
	/**
39
	 * @phpstan-param BaseMapper<EntityType> $mapper
40
	 */
41
	public function __construct(BaseMapper $mapper) {
42
		$this->mapper = $mapper;
43
	}
44
45
	/**
46
	 * Update an entity in the database
47
	 * @phpstan-param EntityType $entity
48
	 * @phpstan-return EntityType
49
	 */
50
	public function update(Entity $entity) : Entity {
51
		return $this->mapper->update($entity);
52
	}
53
54
	/**
55
	 * Delete an entity
56
	 * @param int $id the id of the entity
57
	 * @param string $userId the name of the user for security reasons
58
	 * @throws BusinessLayerException if the entity does not exist or more than one entity exists
59
	 * @phpstan-return EntityType
60
	 */
61
	public function delete(int $id, string $userId) : Entity {
62
		$entity = $this->find($id, $userId);
63
		return $this->mapper->delete($entity);
64
	}
65
66
	/**
67
	 * Deletes entities without specifying the owning user.
68
	 * This should never be called directly from the HTML API, but only in case
69
	 * we can actually trust the passed IDs (e.g. file deleted hook).
70
	 * @param array $ids the ids of the entities which should be deleted
71
	 */
72
	public function deleteById(array $ids) : void {
73
		if (\count($ids) > 0) {
74
			$this->mapper->deleteById($ids);
75
		}
76
	}
77
78
	/**
79
	 * Delete all entities of the given user
80
	 */
81
	public function deleteAll(string $userId) : void {
82
		$this->mapper->deleteAll($userId);
83
	}
84
85
	/**
86
	 * Finds an entity by id
87
	 * @param int $id the id of the entity
88
	 * @param string $userId the name of the user for security reasons
89
	 * @throws BusinessLayerException if the entity does not exist or more than one entity exists
90
	 * @phpstan-return EntityType
91
	 */
92
	public function find(int $id, string $userId) : Entity {
93
		try {
94
			return $this->mapper->find($id, $userId);
95
		} catch (DoesNotExistException $ex) {
96
			throw new BusinessLayerException($ex->getMessage());
97
		} catch (MultipleObjectsReturnedException $ex) {
98
			throw new BusinessLayerException($ex->getMessage());
99
		}
100
	}
101
102
	/**
103
	 * Finds an entity by id, or returns an empty entity instance if the requested one is not found
104
	 * @param int $id the id of the entity
105
	 * @param string $userId the name of the user for security reasons
106
	 * @phpstan-return EntityType
107
	 */
108
	public function findOrDefault(int $id, string $userId) : Entity {
109
		try {
110
			return $this->find($id, $userId);
111
		} catch (BusinessLayerException $ex) {
112
			return $this->mapper->createEntity();
113
		}
114
	}
115
116
	/**
117
	 * Find all entities matching the given IDs.
118
	 * Specifying the user is optional; if omitted, the caller should make sure that
119
	 * user's data is not leaked to unauthorized users.
120
	 * @param integer[] $ids  IDs of the entities to be found
121
	 * @param string|null $userId
122
	 * @param bool $preserveOrder If true, then the result will be in the same order as @a $ids
123
	 * @return Entity[]
124
	 * @phpstan-return EntityType[]
125
	 */
126
	public function findById(array $ids, string $userId=null, bool $preserveOrder=false) : array {
127
		$entities = [];
128
		if (\count($ids) > 0) {
129
			// don't use more than 999 SQL args in one query since that may be a problem for SQLite
130
			$idChunks = \array_chunk($ids, 998);
131
			foreach ($idChunks as $idChunk) {
132
				$entities = \array_merge($entities, $this->mapper->findById($idChunk, $userId));
133
			}
134
		}
135
136
		if ($preserveOrder) {
137
			$lut = Util::createIdLookupTable($entities);
138
			$result = [];
139
			foreach ($ids as $id) {
140
				$result[] = $lut[$id];
141
			}
142
		} else {
143
			$result = $entities;
144
		}
145
146
		return $result;
147
	}
148
149
	/**
150
	 * Finds all entities
151
	 * @param string $userId the name of the user
152
	 * @param integer $sortBy sort order of the result set
153
	 * @param integer|null $limit
154
	 * @param integer|null $offset
155
	 * @param string|null $createdMin Optional minimum `created` timestamp.
156
	 * @param string|null $createdMax Optional maximum `created` timestamp.
157
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
158
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
159
	 * @return Entity[]
160
	 * @phpstan-return EntityType[]
161
	 */
162
	public function findAll(
163
			string $userId, int $sortBy=SortBy::Name, ?int $limit=null, ?int $offset=null,
164
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
165
		return $this->mapper->findAll($userId, $sortBy, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
166
	}
167
168
	/**
169
	 * Return all entities with name matching the search criteria
170
	 * @param string|null $createdMin Optional minimum `created` timestamp.
171
	 * @param string|null $createdMax Optional maximum `created` timestamp.
172
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
173
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
174
	 * @return Entity[]
175
	 * @phpstan-return EntityType[]
176
	 */
177
	public function findAllByName(
178
			?string $name, string $userId, int $matchMode=MatchMode::Exact, ?int $limit=null, ?int $offset=null,
179
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
180
		if ($name !== null) {
181
			$name = \trim($name);
182
		}
183
		return $this->mapper->findAllByName($name, $userId, $matchMode, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
184
	}
185
186
	/**
187
	 * Find all starred entities
188
	 * @return Entity[]
189
	 * @phpstan-return EntityType[]
190
	 */
191
	public function findAllStarred(string $userId, ?int $limit=null, ?int $offset=null) : array {
192
		return $this->mapper->findAllStarred($userId, $limit, $offset);
193
	}
194
195
	/**
196
	 * Find IDSs of all starred entities
197
	 * @return int[]
198
	 */
199
	public function findAllStarredIds(string $userId) : array {
200
		return $this->mapper->findAllStarredIds($userId);
201
	}
202
203
	/**
204
	 * Find all entities with user-given rating 1-5
205
	 * @return Entity[]
206
	 * @phpstan-return EntityType[]
207
	 */
208
	public function findAllRated(string $userId, ?int $limit=null, ?int $offset=null) : array {
209
		return $this->mapper->findAllRated($userId, $limit, $offset);
210
	}
211
212
	/**
213
	 * Find all entities matching multiple criteria, as needed for the Ampache API method `advanced_search`
214
	 * @param string $conjunction Operator to use between the rules, either 'and' or 'or'
215
	 * @param array $rules Array of arrays: [['rule' => string, 'operator' => string, 'input' => string], ...]
216
	 * 				Here, 'rule' has dozens of possible values depending on the business layer in question,
217
	 * 				(see https://ampache.org/api/api-advanced-search#available-search-rules, alias names not supported here),
218
	 * 				'operator' is one of 
219
	 * 				['contain', 'notcontain', 'start', 'end', 'is', 'isnot', 'sounds', 'notsounds', 'regexp', 'notregexp',
220
	 * 				 '>=', '<=', '=', '!=', '>', '<', 'before', 'after', 'true', 'false', 'equal', 'ne', 'limit'],
221
	 * 				'input' is the right side value of the 'operator' (disregarded for the operators 'true' and 'false')
222
	 * @param Random $random When the randomization utility is passed, the result set will be in random order (still supporting proper paging).
223
	 * 						 In this case, the argument $sortBy is ignored.
224
	 * @return Entity[]
225
	 * @phpstan-return EntityType[]
226
	 */
227
	public function findAllAdvanced(
228
			string $conjunction, array $rules, string $userId, int $sortBy=SortBy::Name,
229
			?Random $random=null, ?int $limit=null, ?int $offset=null) : array {
230
231
		if ($conjunction !== 'and' && $conjunction !== 'or') {
232
			throw new BusinessLayerException("Bad conjunction '$conjunction'");
233
		}
234
		try {
235
			if ($random !== null) {
236
				// in case the random order is requested, the limit/offset handling happens after the DB query
237
				$entities = $this->mapper->findAllAdvanced($conjunction, $rules, $userId, SortBy::Name);
238
				$indices = $random->getIndices(\count($entities), $offset, $limit, $userId, 'adv_search_'.$this->mapper->unprefixedTableName());
239
				$entities = Util::arrayMultiGet($entities, $indices);
240
			} else {
241
				$entities = $this->mapper->findAllAdvanced($conjunction, $rules, $userId, $sortBy, $limit, $offset);
242
			}
243
			return $entities;
244
		} catch (\Exception $e) {
245
			// catch everything as many kinds of DB exceptions are possible on various cloud versions
246
			throw new BusinessLayerException($e->getMessage());
247
		}
248
	}
249
250
	/**
251
	 * Find IDs of all user's entities of this kind.
252
	 * Optionally, limit to given IDs which may be used to check the validity of those IDs.
253
	 * @return int[]
254
	 */
255
	public function findAllIds(string $userId, ?array $ids = null) : array {
256
		if ($ids === null || \count($ids) > 0) {
257
			return $this->mapper->findAllIds($userId, $ids);
258
		} else {
259
			return [];
260
		}
261
	}
262
263
	/**
264
	 * Find all entity IDs grouped by the given parent entity IDs. Not applicable on all entity types.
265
	 * @param int[] $parentIds
266
	 * @return array like [parentId => childIds[]]; some parents may have an empty array of children
267
	 * @throws BusinessLayerException if the entity type handled by this business layer doesn't have a parent relation
268
	 */
269
	public function findAllIdsByParentIds(string $userId, array $parentIds) : ?array {
270
		try {
271
			return $this->mapper->findAllIdsByParentIds($userId, $parentIds);
272
		} catch (\DomainException $ex) {
273
			throw new BusinessLayerException($ex->getMessage());
274
		}
275
	}
276
277
	/**
278
	 * Find all IDs and names of user's entities of this kind.
279
	 * Optionally, limit results based on a parent entity (not applicable for all entity types) or update/insert times or name
280
	 * @param bool $excludeChildless Exclude entities having no child-entities if applicable for this business layer (eg. artists without albums)
281
	 * @return array of arrays like ['id' => string, 'name' => string]
282
	 */
283
	public function findAllIdsAndNames(string $userId, IL10N $l10n, ?int $parentId=null, ?int $limit=null, ?int $offset=null,
284
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null,
285
			bool $excludeChildless=false, ?string $name=null) : array {
286
		try {
287
			$idsAndNames = $this->mapper->findAllIdsAndNames(
288
				$userId, $parentId, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax, $excludeChildless, $name);
289
		} catch (\DomainException $ex) {
290
			throw new BusinessLayerException($ex->getMessage());
291
		}
292
		foreach ($idsAndNames as &$idAndName) {
293
			if (empty($idAndName['name'])) {
294
				$emptyEntity = $this->mapper->createEntity();
295
				$idAndName['name'] = $emptyEntity->getNameString($l10n);
296
			}
297
		}
298
		return $idsAndNames;
299
	}
300
301
	/**
302
	 * Find IDs of all users owning any entities of this business layer
303
	 * @return string[]
304
	 */
305
	public function findAllUsers() : array {
306
		return $this->mapper->findAllUsers();
307
	}
308
309
	/**
310
	 * Set the given entities as "starred" on this date
311
	 * @param int[] $ids
312
	 * @param string $userId
313
	 * @return int number of modified entities
314
	 */
315
	public function setStarred(array $ids, string $userId) : int {
316
		if (\count($ids) > 0) {
317
			return $this->mapper->setStarredDate(new \DateTime(), $ids, $userId);
318
		} else {
319
			return 0;
320
		}
321
	}
322
323
	/**
324
	 * Remove the "starred" status of the given entities
325
	 * @param integer[] $ids
326
	 * @param string $userId
327
	 * @return int number of modified entities
328
	 */
329
	public function unsetStarred(array $ids, string $userId) : int {
330
		if (\count($ids) > 0) {
331
			return $this->mapper->setStarredDate(null, $ids, $userId);
332
		} else {
333
			return 0;
334
		}
335
	}
336
337
	/**
338
	 * Tests if entity with given ID and user ID exists in the database
339
	 */
340
	public function exists(int $id, string $userId) : bool {
341
		return $this->mapper->exists($id, $userId);
342
	}
343
344
	/**
345
	 * Get the number of entities
346
	 */
347
	public function count(string $userId) : int {
348
		return $this->mapper->count($userId);
349
	}
350
351
	/**
352
	 * Get the largest entity ID of the user
353
	 */
354
	public function maxId(string $userId) : ?int {
355
		return $this->mapper->maxId($userId);
356
	}
357
358
	/**
359
	 * Get the timestamp of the latest insert operation on the entity type in question
360
	 */
361
	public function latestInsertTime(string $userId) : \DateTime {
362
		return $this->mapper->latestInsertTime($userId) ?? new \DateTime('1970-01-01');
363
	}
364
365
	/**
366
	 * Get the timestamp of the latest update operation on the entity type in question
367
	 */
368
	public function latestUpdateTime(string $userId) : \DateTime {
369
		return $this->mapper->latestUpdateTime($userId) ?? new \DateTime('1970-01-01');
370
	}
371
}
372