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

PersonMapper::deleteOrphaned()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 2.0373

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 18
c 3
b 0
f 0
nc 2
nop 1
dl 0
loc 22
ccs 15
cts 19
cp 0.7895
crap 2.0373
rs 9.6666
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