Passed
Push — self-contained-model ( 586004...ef7b10 )
by Matias
03:56
created

PersonMapper::mergeClusterToDatabase()   C

Complexity

Conditions 14
Paths 216

Size

Total Lines 107
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 51
CRAP Score 14.0336

Importance

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