Passed
Push — self-contained-model ( 296628...6cfb69 )
by Matias
04:09
created

PersonMapper::deleteUserPersons()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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