Passed
Pull Request — master (#178)
by Branko
06:23 queued 04:49
created

PersonMapper::mergeClusterToDatabase()   C

Complexity

Conditions 14
Paths 210

Size

Total Lines 99
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 14.0478

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 14
eloc 51
c 2
b 0
f 0
nc 210
nop 3
dl 0
loc 99
ccs 45
cts 48
cp 0.9375
crap 14.0478
rs 5.3083

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