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