Passed
Push — split-cluster-person ( 7ada7e...098531 )
by Matias
05:26
created

PersonMapper::mergeClusterToDatabase()   C

Complexity

Conditions 14
Paths 216

Size

Total Lines 106
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 14.0355

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 14
eloc 57
c 3
b 0
f 0
nc 216
nop 3
dl 0
loc 106
ccs 50
cts 53
cp 0.9434
crap 14.0355
rs 5.2333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2018-2020, 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 6
	public function find(string $userId, int $personId) {
46 6
		$qb = $this->db->getQueryBuilder();
47 6
		$qb->select('id', 'name')
48 6
			->from($this->getTableName(), 'p')
49 6
			->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)))
50 6
			->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
51 6
		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
	public function findByName(string $userId, int $modelId, string $personName): array {
61
		$sub = $this->db->getQueryBuilder();
62
		$sub->select(new Literal('1'))
63
			->from('facerecog_faces', 'f')
64
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
65
			->where($sub->expr()->eq('p.id', 'f.person'))
66
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
67
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')))
68
			->andwhere($sub->expr()->eq('p.name', $sub->createParameter('person_name')));
69
70
		$qb = $this->db->getQueryBuilder();
71
		$qb->select('id', 'name', 'is_valid')
72
			->from($this->getTableName(), 'p')
73
			->where('EXISTS (' . $sub->getSQL() . ')')
74
			->setParameter('user_id', $userId)
75
			->setParameter('model_id', $modelId)
76
			->setParameter('person_name', $personName);
77
78
		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);
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
	 * Returns count of persons (clusters) found for a given user.
151
	 *
152
	 * @param string $userId ID of the user
153
	 * @param int $modelId ID of the model
154
	 * @param bool $onlyInvalid True if client wants count of invalid persons only,
155
	 *  false if client want count of all persons
156
	 * @return int Count of persons
157
	 */
158 15
	public function countPersons(string $userId, int $modelId, bool $onlyInvalid=false): int {
159 15
		$sub = $this->db->getQueryBuilder();
160 15
		$sub->select(new Literal('1'))
161 15
			->from('facerecog_faces', 'f')
162 15
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
163 15
			->where($sub->expr()->eq('p.id', 'f.person'))
164 15
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
165 15
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
166
167 15
		$qb = $this->db->getQueryBuilder();
168 15
		$qb->select($qb->createFunction('COUNT(' . $qb->getColumnName('id') . ')'))
169 15
			->from($this->getTableName(), 'p')
170 15
			->where('EXISTS (' . $sub->getSQL() . ')');
171
172 15
		if ($onlyInvalid) {
173
			$qb = $qb
174
				->andWhere($qb->expr()->eq('is_valid', $qb->createParameter('is_valid')))
175
				->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL);
176
		}
177
178
		$qb = $qb
179 15
			->setParameter('user_id', $userId)
180 15
			->setParameter('model_id', $modelId);
181
182 15
		$resultStatement = $qb->execute();
183 15
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
184 15
		$resultStatement->closeCursor();
185
186 15
		return (int)$data[0];
187
	}
188
189
	/**
190
	 * Based on a given image, takes all faces that belong to that image
191
	 * and invalidates all person that those faces belongs to.
192
	 *
193
	 * @param int $imageId ID of image for which to invalidate persons for
194
	 */
195 12
	public function invalidatePersons(int $imageId) {
196 12
		$sub = $this->db->getQueryBuilder();
197 12
		$tableNameWithPrefixWithoutQuotes = trim($sub->getTableName($this->getTableName()), '`');
198 12
		$sub->select(new Literal('1'));
199 12
		$sub->from('facerecog_images', 'i')
200 12
			->innerJoin('i', 'facerecog_faces' ,'f', $sub->expr()->eq('i.id', 'f.image'))
201 12
			->where($sub->expr()->eq($tableNameWithPrefixWithoutQuotes . '.id', 'f.person'))
202 12
			->andWhere($sub->expr()->eq('i.id', $sub->createParameter('image_id')));
203
204 12
		$qb = $this->db->getQueryBuilder();
205 12
		$qb->update($this->getTableName())
206 12
			->set("is_valid", $qb->createParameter('is_valid'))
207 12
			->where('EXISTS (' . $sub->getSQL() . ')')
208 12
			->setParameter('image_id', $imageId)
209 12
			->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL)
210 12
			->execute();
211 12
	}
212
213
	/**
214
	 * Based on current clusters and new clusters, do database reconciliation.
215
	 * It tries to do that in minimal number of SQL queries. Operation is atomic.
216
	 *
217
	 * Clusters are array, where keys are ID of persons, and values are indexed arrays
218
	 * with values that are ID of the faces for those persons.
219
	 *
220
	 * @param string $userId ID of the user that clusters belong to
221
	 * @param array $currentClusters Current clusters
222
	 * @param array $newClusters New clusters
223
	 */
224 14
	public function mergeClusterToDatabase(string $userId, $currentClusters, $newClusters) {
225 14
		$this->db->beginTransaction();
226 14
		$currentDateTime = new \DateTime();
227
228
		try {
229
			// Delete clusters that do not exist anymore
230 14
			foreach($currentClusters as $oldPerson => $oldFaces) {
231 11
				if (array_key_exists($oldPerson, $newClusters)) {
232 6
					continue;
233
				}
234
235
				// OK, we bumped into cluster that existed and now it does not exist.
236
				// We need to remove all references to it and to delete it.
237 7
				foreach ($oldFaces as $oldFace) {
238 7
					$this->updateFace($oldFace, null);
239
				}
240
241
				// todo: this is not very cool. What if user had associated linked user to this. And all lost?
242 7
				$qb = $this->db->getQueryBuilder();
243
				// todo: for extra safety, we should probably add here additional condition, where (user=$userId)
244
				$qb
245 7
					->delete($this->getTableName())
246 7
					->where($qb->expr()->eq('id', $qb->createNamedParameter($oldPerson)))
247 7
					->execute();
248
			}
249
250
			// Modify existing clusters
251 14
			foreach($newClusters as $newPerson=>$newFaces) {
252 12
				if (!array_key_exists($newPerson, $currentClusters)) {
253
					// This cluster didn't exist, there is nothing to modify
254
					// It will be processed during cluster adding operation
255 9
					continue;
256
				}
257
258 6
				$oldFaces = $currentClusters[$newPerson];
259 6
				if ($newFaces === $oldFaces) {
260
					// Set cluster as valid now
261 2
					$qb = $this->db->getQueryBuilder();
262
					$qb
263 2
						->update($this->getTableName())
264 2
						->set("is_valid", $qb->createParameter('is_valid'))
265 2
						->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
266 2
						->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
267 2
						->execute();
268 2
					continue;
269
				}
270
271
				// OK, set of faces do differ. Now, we could potentially go into finer grain details
272
				// and add/remove each individual face, but this seems too detailed. Enough is to
273
				// reset all existing faces to null and to add new faces to new person. That should
274
				// take care of both faces that are removed from cluster, as well as for newly added
275
				// faces to this cluster.
276
277
				// First remove all old faces from any cluster (reset them to null)
278 5
				foreach ($oldFaces as $oldFace) {
279
					// Reset face to null only if it wasn't moved to other cluster!
280
					// (if face is just moved to other cluster, do not reset to null, as some other
281
					// pass for some other cluster will eventually update it to proper cluster)
282 5
					if ($this->isFaceInClusters($oldFace, $newClusters) === false) {
283 1
						$this->updateFace($oldFace, null);
284
					}
285
				}
286
287
				// Then set all new faces to belong to this cluster
288 5
				foreach ($newFaces as $newFace) {
289 5
					$this->updateFace($newFace, $newPerson);
290
				}
291
292
				// Set cluster as valid now
293 5
				$qb = $this->db->getQueryBuilder();
294
				$qb
295 5
					->update($this->getTableName())
296 5
					->set("is_valid", $qb->createParameter('is_valid'))
297 5
					->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
298 5
					->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
299 5
					->execute();
300
			}
301
302
			// Add new clusters
303 14
			foreach($newClusters as $newPerson=>$newFaces) {
304 12
				if (array_key_exists($newPerson, $currentClusters)) {
305
					// This cluster already existed, nothing to add
306
					// It was already processed during modify cluster operation
307 6
					continue;
308
				}
309
310
				// Create new cluster and add all faces to it
311 9
				$qb = $this->db->getQueryBuilder();
312
				$qb
313 9
					->insert($this->getTableName())
314 9
					->values([
315 9
						'user' => $qb->createNamedParameter($userId),
316 9
						'is_valid' => $qb->createNamedParameter(true),
317 9
						'last_generation_time' => $qb->createNamedParameter($currentDateTime, IQueryBuilder::PARAM_DATE),
318 9
						'linked_user' => $qb->createNamedParameter(null)])
319 9
					->execute();
320 9
				$insertedPersonId = $this->db->lastInsertId($this->getTableName());
321 9
				foreach ($newFaces as $newFace) {
322 9
					$this->updateFace($newFace, $insertedPersonId);
323
				}
324
			}
325
326 14
			$this->db->commit();
327
		} catch (\Exception $e) {
328
			$this->db->rollBack();
329
			throw $e;
330
		}
331 14
	}
332
333
	/**
334
	 * Deletes all persons from that user.
335
	 *
336
	 * @param string $userId User to drop persons from a table.
337
	 */
338 28
	public function deleteUserPersons(string $userId) {
339 28
		$qb = $this->db->getQueryBuilder();
340 28
		$qb->delete($this->getTableName())
341 28
			->where($qb->expr()->eq('user', $qb->createNamedParameter($userId)))
342 28
			->execute();
343 28
	}
344
345
	/**
346
	 * Deletes all persons from that user and model
347
	 *
348
	 * @param string $userId ID of user for drop from table
349
	 * @param string $modelId model for drop from table
350
	 */
351
	public function deleteUserModel(string $userId, int $modelId) {
352
		//TODO: Make it atomic
353
		$qb = $this->db->getQueryBuilder();
354
		$qb->delete($this->getTableName())
355
			->where($qb->expr()->eq('id', $qb->createParameter('person')));
356
357
		$persons = $this->findAll($userId, $modelId);
358
		foreach ($persons as $person) {
359
			$qb->setParameter('person', $person->getId())->execute();
360
		}
361
	}
362
363
	/**
364
	 * Deletes person if it is empty (have no faces associated to it)
365
	 *
366
	 * @param int $personId Person to check if it should be deleted
367
	 */
368
	public function removeIfEmpty(int $personId) {
369
		$sub = $this->db->getQueryBuilder();
370
		$sub->select(new Literal('1'));
371
		$sub->from('facerecog_faces', 'f')
372
			->where($sub->expr()->eq('f.person', $sub->createParameter('person')));
373
374
		$qb = $this->db->getQueryBuilder();
375
		$qb->delete($this->getTableName())
376
			->where($qb->expr()->eq('id', $qb->createParameter('person')))
377
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
378
			->setParameter('person', $personId)
379
			->execute();
380
	}
381
382
	/**
383
	 * Deletes all persons that have no faces associated to them
384
	 *
385
	 * @param string $userId ID of user for which we are deleting orphaned persons
386
	 */
387 1
	public function deleteOrphaned(string $userId): int {
388 1
		$sub = $this->db->getQueryBuilder();
389 1
		$sub->select(new Literal('1'));
390 1
		$sub->from('facerecog_faces', 'f')
391 1
			->where($sub->expr()->eq('f.person', 'p.id'));
392
393 1
		$qb = $this->db->getQueryBuilder();
394 1
		$qb->select('p.id')
395 1
			->from($this->getTableName(), 'p')
396 1
			->where($qb->expr()->eq('p.user', $qb->createParameter('user')))
397 1
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
398 1
			->setParameter('user', $userId);
399 1
		$orphanedPersons = $this->findEntities($qb);
400
401 1
		$orphaned = 0;
402 1
		foreach ($orphanedPersons as $person) {
403
			$qb = $this->db->getQueryBuilder();
404
			$orphaned += $qb->delete($this->getTableName())
405
				->where($qb->expr()->eq('id', $qb->createNamedParameter($person->id)))
406
				->execute();
407
		}
408 1
		return $orphaned;
409
	}
410
411
	/**
412
	 * Updates one face with $faceId to database to person ID $personId.
413
	 *
414
	 * @param int $faceId ID of the face
415
	 * @param int|null $personId ID of the person
416
	 */
417 12
	private function updateFace(int $faceId, $personId) {
418 12
		$qb = $this->db->getQueryBuilder();
419 12
		$qb->update('facerecog_faces')
420 12
			->set("person", $qb->createNamedParameter($personId))
421 12
			->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
422 12
			->execute();
423 12
	}
424
425
	/**
426
	 * Checks if face with a given ID is in any cluster.
427
	 *
428
	 * @param int $faceId ID of the face to check
429
	 * @param array $cluster All clusters to check into
430
	 *
431
	 * @return bool True if face is found in any cluster, false otherwise.
432
	 */
433 5
	private function isFaceInClusters(int $faceId, array $clusters): bool {
434 5
		foreach ($clusters as $_=>$faces) {
435 5
			if (in_array($faceId, $faces)) {
436 5
				return true;
437
			}
438
		}
439 1
		return false;
440
	}
441
}
442