Passed
Push — nc21sidebar ( a11006...67b2b6 )
by Matias
05:56
created

PersonMapper::findPersonsLike()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 14
c 0
b 0
f 0
nc 1
nop 5
dl 0
loc 18
ccs 0
cts 15
cp 0
crap 2
rs 9.7998
1
<?php
2
/**
3
 * @copyright Copyright (c) 2018-2021, Matias De lellis <[email protected]>
4
 * @copyright Copyright (c) 2018-2019, Branko Kokanovic <[email protected]>
5
 *
6
 * @author Branko Kokanovic <[email protected]>
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 *
23
 */
24
namespace OCA\FaceRecognition\Db;
25
26
use OC\DB\QueryBuilder\Literal;
0 ignored issues
show
Bug introduced by
The type OC\DB\QueryBuilder\Literal was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
28
use OCP\IDBConnection;
29
use OCP\IUser;
30
31
use OCP\AppFramework\Db\QBMapper;
32
use OCP\AppFramework\Db\DoesNotExistException;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
35
class PersonMapper extends QBMapper {
36
37 1
	public function __construct(IDBConnection $db) {
38 1
		parent::__construct($db, 'facerecog_persons', '\OCA\FaceRecognition\Db\Person');
39 1
	}
40
41
	/**
42
	 * @param string $userId ID of the user
43
	 * @param int $personId ID of the person
44
	 */
45 8
	public function find(string $userId, int $personId) {
46 8
		$qb = $this->db->getQueryBuilder();
47 8
		$qb->select('id', 'name')
48 8
			->from($this->getTableName(), 'p')
49 8
			->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)))
50 8
			->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
51 8
		return $this->findEntity($qb);
52
	}
53
54
	/**
55
	 * @param string $userId ID of the user
56
	 * @param int $modelId ID of the model
57
	 * @param string $personName name of the person to find
58
	 * @return Person[]
59
	 */
60 2
	public function findByName(string $userId, int $modelId, string $personName): array {
61 2
		$sub = $this->db->getQueryBuilder();
62 2
		$sub->select(new Literal('1'))
63 2
			->from('facerecog_faces', 'f')
64 2
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
65 2
			->where($sub->expr()->eq('p.id', 'f.person'))
66 2
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
67 2
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')))
68 2
			->andwhere($sub->expr()->eq('p.name', $sub->createParameter('person_name')));
69
70 2
		$qb = $this->db->getQueryBuilder();
71 2
		$qb->select('id', 'name', 'is_valid')
72 2
			->from($this->getTableName(), 'p')
73 2
			->where('EXISTS (' . $sub->getSQL() . ')')
74 2
			->setParameter('user_id', $userId)
75 2
			->setParameter('model_id', $modelId)
76 2
			->setParameter('person_name', $personName);
77
78 2
		return $this->findEntities($qb);
79
	}
80
81
	/**
82
	 * @param string $userId ID of the user
83
	 * @param int $modelId ID of the model
84
	 * @return Person[]
85
	 */
86
	public function findUnassigned(string $userId, int $modelId): array {
87
		$sub = $this->db->getQueryBuilder();
88
		$sub->select(new Literal('1'))
89
			->from('facerecog_faces', 'f')
90
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
91
			->where($sub->expr()->eq('p.id', 'f.person'))
92
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
93
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
94
95
		$qb = $this->db->getQueryBuilder();
96
		$qb->select('id', 'is_valid')
97
			->from($this->getTableName(), 'p')
98
			->where('EXISTS (' . $sub->getSQL() . ')')
99
			->andWhere($qb->expr()->eq('is_valid', $qb->createParameter('is_valid')))
100
			->andWhere($qb->expr()->isNull('name'))
101
			->setParameter('user_id', $userId)
102
			->setParameter('model_id', $modelId)
103
			->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL);
104
105
		return $this->findEntities($qb);
106
	}
107
108
	/**
109
	 * @param string $userId ID of the user
110
	 * @param int $modelId ID of the model
111
	 * @return Person[]
112
	 */
113 13
	public function findAll(string $userId, int $modelId): array {
114 13
		$sub = $this->db->getQueryBuilder();
115 13
		$sub->select(new Literal('1'))
116 13
			->from('facerecog_faces', 'f')
117 13
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
118 13
			->where($sub->expr()->eq('p.id', 'f.person'))
119 13
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
120 13
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
121
122 13
		$qb = $this->db->getQueryBuilder();
123 13
		$qb->select('id', 'name', 'is_valid')
124 13
			->from($this->getTableName(), 'p')
125 13
			->where('EXISTS (' . $sub->getSQL() . ')')
126 13
			->setParameter('user_id', $userId)
127 13
			->setParameter('model_id', $modelId);
128
129 13
		return $this->findEntities($qb);
130
	}
131
132
	/**
133
	 * @param string $userId ID of the user
134
	 */
135
	public function findDistinctNames(string $userId, int $modelId) {
136
		$qb = $this->db->getQueryBuilder();
137
		$qb->selectDistinct('name')
138
			->from($this->getTableName(), 'p')
139
			->innerJoin('p', 'facerecog_faces' , 'f', $qb->expr()->eq('f.person', 'p.id'))
140
			->innerJoin('f', 'facerecog_images' ,'i', $qb->expr()->eq('f.image', 'i.id'))
141
			->where($qb->expr()->eq('i.user', $qb->createParameter('user_id')))
142
			->andWhere($qb->expr()->eq('i.model', $qb->createParameter('model_id')))
143
			->andwhere($qb->expr()->isNotNull('p.name'))
144
			->setParameter('user_id', $userId)
145
			->setParameter('model_id', $modelId);
146
		return $this->findEntities($qb);
147
	}
148
149
	/**
150
	 * Search Person by name
151
	 *
152
	 */
153
	public function findPersonsLike(string $userId, int $modelId, string $name, $offset = null, $limit = null): array {
154
		$qb = $this->db->getQueryBuilder();
155
		$qb->selectDistinct('p.name')
156
			->from($this->getTableName(), 'p')
157
			->innerJoin('p', 'facerecog_faces', 'f', $qb->expr()->eq('f.person', 'p.id'))
158
			->innerJoin('p', 'facerecog_images', 'i', $qb->expr()->eq('f.image', 'i.id'))
159
			->where($qb->expr()->eq('p.user', $qb->createNamedParameter($userId)))
160
			->andWhere($qb->expr()->eq('model', $qb->createNamedParameter($modelId)))
161
			->andWhere($qb->expr()->eq('is_processed', $qb->createNamedParameter(True)))
162
			->andWhere($qb->expr()->like($qb->func()->lower('p.name'), $qb->createParameter('query')));
163
164
		$query = '%' . $this->db->escapeLikeParameter(strtolower($name)) . '%';
165
		$qb->setParameter('query', $query);
166
167
		$qb->setFirstResult($offset);
168
		$qb->setMaxResults($limit);
169
170
		return $this->findEntities($qb);
171
	}
172
173
	/**
174
	 * Returns count of persons (clusters) found for a given user.
175
	 *
176
	 * @param string $userId ID of the user
177
	 * @param int $modelId ID of the model
178
	 * @param bool $onlyInvalid True if client wants count of invalid persons only,
179
	 *  false if client want count of all persons
180
	 * @return int Count of persons
181
	 */
182 15
	public function countPersons(string $userId, int $modelId, bool $onlyInvalid=false): int {
183 15
		$sub = $this->db->getQueryBuilder();
184 15
		$sub->select(new Literal('1'))
185 15
			->from('facerecog_faces', 'f')
186 15
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
187 15
			->where($sub->expr()->eq('p.id', 'f.person'))
188 15
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
189 15
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
190
191 15
		$qb = $this->db->getQueryBuilder();
192 15
		$qb->select($qb->createFunction('COUNT(' . $qb->getColumnName('id') . ')'))
193 15
			->from($this->getTableName(), 'p')
194 15
			->where('EXISTS (' . $sub->getSQL() . ')');
195
196 15
		if ($onlyInvalid) {
197
			$qb = $qb
198
				->andWhere($qb->expr()->eq('is_valid', $qb->createParameter('is_valid')))
199
				->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL);
200
		}
201
202
		$qb = $qb
203 15
			->setParameter('user_id', $userId)
204 15
			->setParameter('model_id', $modelId);
205
206 15
		$resultStatement = $qb->execute();
207 15
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
208 15
		$resultStatement->closeCursor();
209
210 15
		return (int)$data[0];
211
	}
212
213
	/**
214
	 * Based on a given image, takes all faces that belong to that image
215
	 * and invalidates all person that those faces belongs to.
216
	 *
217
	 * @param int $imageId ID of image for which to invalidate persons for
218
	 */
219 12
	public function invalidatePersons(int $imageId) {
220 12
		$sub = $this->db->getQueryBuilder();
221 12
		$tableNameWithPrefixWithoutQuotes = trim($sub->getTableName($this->getTableName()), '`');
222 12
		$sub->select(new Literal('1'));
223 12
		$sub->from('facerecog_images', 'i')
224 12
			->innerJoin('i', 'facerecog_faces' ,'f', $sub->expr()->eq('i.id', 'f.image'))
225 12
			->where($sub->expr()->eq($tableNameWithPrefixWithoutQuotes . '.id', 'f.person'))
226 12
			->andWhere($sub->expr()->eq('i.id', $sub->createParameter('image_id')));
227
228 12
		$qb = $this->db->getQueryBuilder();
229 12
		$qb->update($this->getTableName())
230 12
			->set("is_valid", $qb->createParameter('is_valid'))
231 12
			->where('EXISTS (' . $sub->getSQL() . ')')
232 12
			->setParameter('image_id', $imageId)
233 12
			->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL)
234 12
			->execute();
235 12
	}
236
237
	/**
238
	 * Based on current clusters and new clusters, do database reconciliation.
239
	 * It tries to do that in minimal number of SQL queries. Operation is atomic.
240
	 *
241
	 * Clusters are array, where keys are ID of persons, and values are indexed arrays
242
	 * with values that are ID of the faces for those persons.
243
	 *
244
	 * @param string $userId ID of the user that clusters belong to
245
	 * @param array $currentClusters Current clusters
246
	 * @param array $newClusters New clusters
247
	 */
248 14
	public function mergeClusterToDatabase(string $userId, $currentClusters, $newClusters) {
249 14
		$this->db->beginTransaction();
250 14
		$currentDateTime = new \DateTime();
251
252
		try {
253
			// Delete clusters that do not exist anymore
254 14
			foreach($currentClusters as $oldPerson => $oldFaces) {
255 11
				if (array_key_exists($oldPerson, $newClusters)) {
256 6
					continue;
257
				}
258
259
				// OK, we bumped into cluster that existed and now it does not exist.
260
				// We need to remove all references to it and to delete it.
261 7
				foreach ($oldFaces as $oldFace) {
262 7
					$this->updateFace($oldFace, null);
263
				}
264
265
				// todo: this is not very cool. What if user had associated linked user to this. And all lost?
266 7
				$qb = $this->db->getQueryBuilder();
267
				// todo: for extra safety, we should probably add here additional condition, where (user=$userId)
268
				$qb
269 7
					->delete($this->getTableName())
270 7
					->where($qb->expr()->eq('id', $qb->createNamedParameter($oldPerson)))
271 7
					->execute();
272
			}
273
274
			// Modify existing clusters
275 14
			foreach($newClusters as $newPerson=>$newFaces) {
276 12
				if (!array_key_exists($newPerson, $currentClusters)) {
277
					// This cluster didn't exist, there is nothing to modify
278
					// It will be processed during cluster adding operation
279 9
					continue;
280
				}
281
282 6
				$oldFaces = $currentClusters[$newPerson];
283 6
				if ($newFaces === $oldFaces) {
284
					// Set cluster as valid now
285 2
					$qb = $this->db->getQueryBuilder();
286
					$qb
287 2
						->update($this->getTableName())
288 2
						->set("is_valid", $qb->createParameter('is_valid'))
289 2
						->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
290 2
						->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
291 2
						->execute();
292 2
					continue;
293
				}
294
295
				// OK, set of faces do differ. Now, we could potentially go into finer grain details
296
				// and add/remove each individual face, but this seems too detailed. Enough is to
297
				// reset all existing faces to null and to add new faces to new person. That should
298
				// take care of both faces that are removed from cluster, as well as for newly added
299
				// faces to this cluster.
300
301
				// First remove all old faces from any cluster (reset them to null)
302 5
				foreach ($oldFaces as $oldFace) {
303
					// Reset face to null only if it wasn't moved to other cluster!
304
					// (if face is just moved to other cluster, do not reset to null, as some other
305
					// pass for some other cluster will eventually update it to proper cluster)
306 5
					if ($this->isFaceInClusters($oldFace, $newClusters) === false) {
307 1
						$this->updateFace($oldFace, null);
308
					}
309
				}
310
311
				// Then set all new faces to belong to this cluster
312 5
				foreach ($newFaces as $newFace) {
313 5
					$this->updateFace($newFace, $newPerson);
314
				}
315
316
				// Set cluster as valid now
317 5
				$qb = $this->db->getQueryBuilder();
318
				$qb
319 5
					->update($this->getTableName())
320 5
					->set("is_valid", $qb->createParameter('is_valid'))
321 5
					->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
322 5
					->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
323 5
					->execute();
324
			}
325
326
			// Add new clusters
327 14
			foreach($newClusters as $newPerson=>$newFaces) {
328 12
				if (array_key_exists($newPerson, $currentClusters)) {
329
					// This cluster already existed, nothing to add
330
					// It was already processed during modify cluster operation
331 6
					continue;
332
				}
333
334
				// Create new cluster and add all faces to it
335 9
				$qb = $this->db->getQueryBuilder();
336
				$qb
337 9
					->insert($this->getTableName())
338 9
					->values([
339 9
						'user' => $qb->createNamedParameter($userId),
340 9
						'is_valid' => $qb->createNamedParameter(true),
341 9
						'last_generation_time' => $qb->createNamedParameter($currentDateTime, IQueryBuilder::PARAM_DATE),
342 9
						'linked_user' => $qb->createNamedParameter(null)])
343 9
					->execute();
344 9
				$insertedPersonId = $this->db->lastInsertId($this->getTableName());
0 ignored issues
show
Deprecated Code introduced by
The function OCP\IDBConnection::lastInsertId() has been deprecated: 21.0.0 use \OCP\DB\QueryBuilder\IQueryBuilder::getLastInsertId ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

344
				$insertedPersonId = /** @scrutinizer ignore-deprecated */ $this->db->lastInsertId($this->getTableName());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
345 9
				foreach ($newFaces as $newFace) {
346 9
					$this->updateFace($newFace, $insertedPersonId);
347
				}
348
			}
349
350 14
			$this->db->commit();
351
		} catch (\Exception $e) {
352
			$this->db->rollBack();
353
			throw $e;
354
		}
355 14
	}
356
357
	/**
358
	 * Deletes all persons from that user.
359
	 *
360
	 * @param string $userId User to drop persons from a table.
361
	 */
362 28
	public function deleteUserPersons(string $userId) {
363 28
		$qb = $this->db->getQueryBuilder();
364 28
		$qb->delete($this->getTableName())
365 28
			->where($qb->expr()->eq('user', $qb->createNamedParameter($userId)))
366 28
			->execute();
367 28
	}
368
369
	/**
370
	 * Deletes all persons from that user and model
371
	 *
372
	 * @param string $userId ID of user for drop from table
373
	 * @param string $modelId model for drop from table
374
	 */
375
	public function deleteUserModel(string $userId, int $modelId) {
376
		//TODO: Make it atomic
377
		$qb = $this->db->getQueryBuilder();
378
		$qb->delete($this->getTableName())
379
			->where($qb->expr()->eq('id', $qb->createParameter('person')));
380
381
		$persons = $this->findAll($userId, $modelId);
382
		foreach ($persons as $person) {
383
			$qb->setParameter('person', $person->getId())->execute();
384
		}
385
	}
386
387
	/**
388
	 * Deletes person if it is empty (have no faces associated to it)
389
	 *
390
	 * @param int $personId Person to check if it should be deleted
391
	 */
392
	public function removeIfEmpty(int $personId) {
393
		$sub = $this->db->getQueryBuilder();
394
		$sub->select(new Literal('1'));
395
		$sub->from('facerecog_faces', 'f')
396
			->where($sub->expr()->eq('f.person', $sub->createParameter('person')));
397
398
		$qb = $this->db->getQueryBuilder();
399
		$qb->delete($this->getTableName())
400
			->where($qb->expr()->eq('id', $qb->createParameter('person')))
401
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
402
			->setParameter('person', $personId)
403
			->execute();
404
	}
405
406
	/**
407
	 * Deletes all persons that have no faces associated to them
408
	 *
409
	 * @param string $userId ID of user for which we are deleting orphaned persons
410
	 */
411 1
	public function deleteOrphaned(string $userId): int {
412 1
		$sub = $this->db->getQueryBuilder();
413 1
		$sub->select(new Literal('1'));
414 1
		$sub->from('facerecog_faces', 'f')
415 1
			->where($sub->expr()->eq('f.person', 'p.id'));
416
417 1
		$qb = $this->db->getQueryBuilder();
418 1
		$qb->select('p.id')
419 1
			->from($this->getTableName(), 'p')
420 1
			->where($qb->expr()->eq('p.user', $qb->createParameter('user')))
421 1
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
422 1
			->setParameter('user', $userId);
423 1
		$orphanedPersons = $this->findEntities($qb);
424
425 1
		$orphaned = 0;
426 1
		foreach ($orphanedPersons as $person) {
427
			$qb = $this->db->getQueryBuilder();
428
			$orphaned += $qb->delete($this->getTableName())
429
				->where($qb->expr()->eq('id', $qb->createNamedParameter($person->id)))
430
				->execute();
431
		}
432 1
		return $orphaned;
433
	}
434
435
	/**
436
	 * Updates one face with $faceId to database to person ID $personId.
437
	 *
438
	 * @param int $faceId ID of the face
439
	 * @param int|null $personId ID of the person
440
	 */
441 12
	private function updateFace(int $faceId, $personId) {
442 12
		$qb = $this->db->getQueryBuilder();
443 12
		$qb->update('facerecog_faces')
444 12
			->set("person", $qb->createNamedParameter($personId))
445 12
			->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
446 12
			->execute();
447 12
	}
448
449
	/**
450
	 * Checks if face with a given ID is in any cluster.
451
	 *
452
	 * @param int $faceId ID of the face to check
453
	 * @param array $cluster All clusters to check into
454
	 *
455
	 * @return bool True if face is found in any cluster, false otherwise.
456
	 */
457 5
	private function isFaceInClusters(int $faceId, array $clusters): bool {
458 5
		foreach ($clusters as $_=>$faces) {
459 5
			if (in_array($faceId, $faces)) {
460 5
				return true;
461
			}
462
		}
463 1
		return false;
464
	}
465
}
466