Passed
Push — recreate-cluster-logic ( 25ad8b )
by Branko
01:54
created

PersonMapper::findFromFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 2
dl 0
loc 16
ccs 0
cts 13
cp 0
crap 2
rs 9.8666
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\Mapper;
32
use OCP\AppFramework\Db\DoesNotExistException;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
35
class PersonMapper extends Mapper {
36
37 4
	public function __construct(IDBConnection $db) {
38 4
		parent::__construct($db, 'face_recognition_persons', '\OCA\FaceRecognition\Db\Person');
39 4
	}
40
41
42
	public function find (string $userId, int $personId): Person {
43
		$qb = $this->db->getQueryBuilder();
44
		$qb->select('id', 'name')
0 ignored issues
show
Unused Code introduced by
The call to OCP\DB\QueryBuilder\IQueryBuilder::select() has too many arguments starting with 'name'. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

44
		$qb->/** @scrutinizer ignore-call */ 
45
       select('id', 'name')

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
45
			->from('face_recognition_persons', 'p')
46
			->where($qb->expr()->eq('id', $qb->createParameter('person_id')))
47
			->andWhere($qb->expr()->eq('user', $qb->createParameter('user_id')));
48
49
		$params = array();
50
		$params['person_id'] = $personId;
51
		$params['user_id'] = $userId;
52
53
		$person = $this->findEntity($qb->getSQL(), $params);
54
		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...
55
	}
56
57
	/**
58
	 * Returns count of persons (clusters) found for a given user.
59
	 *
60
	 * @param string $userId ID of the user
61
	 * @param bool $onlyInvalid True if client wants count of invalid persons only,
62
	 *  false if client want count of all persons
63
	 * @return int Count of persons
64
	 */
65
	public function countPersons(string $userId, bool $onlyInvalid=false): int {
66
		$qb = $this->db->getQueryBuilder();
67
		$qb = $qb
68
			->select($qb->createFunction('COUNT(' . $qb->getColumnName('id') . ')'))
69
			->from($this->getTableName())
70
			->where($qb->expr()->eq('user', $qb->createParameter('user')));
71
		if ($onlyInvalid) {
72
			$qb = $qb
73
				->andWhere($qb->expr()->eq('is_valid', $qb->createParameter('is_valid')))
74
				->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL);
75
		}
76
		$query = $qb->setParameter('user', $userId);
77
		$resultStatement = $query->execute();
78
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
79
		$resultStatement->closeCursor();
80
81
		return (int)$data[0];
82
	}
83
84
	/**
85
	 * Based on a given fileId, takes all person that belong to that image
86
	 * and return an array with that.
87
	 *
88
	 * @param string $userId ID of the user that clusters belong to
89
	 * @param int $fileId ID of file image for which to searh persons.
90
	 *
91
	 * @return array of persons
92
	 */
93
	public function findFromFile(string $userId, int $fileId): array {
94
		$qb = $this->db->getQueryBuilder();
95
		$qb->select('p.id', 'name');
0 ignored issues
show
Unused Code introduced by
The call to OCP\DB\QueryBuilder\IQueryBuilder::select() has too many arguments starting with 'name'. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

95
		$qb->/** @scrutinizer ignore-call */ 
96
       select('p.id', 'name');

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
96
		$qb->from("face_recognition_persons", "p")
97
			->innerJoin('p', 'face_recognition_faces' ,'f', $qb->expr()->eq('p.id', 'f.person'))
98
			->innerJoin('p', 'face_recognition_images' ,'i', $qb->expr()->eq('i.id', 'f.image'))
99
			->where($qb->expr()->eq('p.user', $qb->createParameter('user')))
100
			->andWhere($qb->expr()->eq('i.file', $qb->createParameter('file_id')));
101
102
		$params = array();
103
		$params['user'] = $userId;
104
		$params['file_id'] = $fileId;
105
106
		$persons = $this->findEntities($qb->getSQL(), $params);
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
	public function invalidatePersons(int $imageId) {
118
		$sub = $this->db->getQueryBuilder();
119
		$sub->select(new Literal('1'));
120
		$sub->from("face_recognition_images", "i")
121
			->innerJoin('i', 'face_recognition_faces' ,'f', $sub->expr()->eq('i.id', 'f.image'))
122
			->where($sub->expr()->eq('p.id', 'f.person'))
123
			->andWhere($sub->expr()->eq('i.id', $sub->createParameter('image_id')));
124
125
		$qb = $this->db->getQueryBuilder();
126
		$qb->update($this->getTableName(), 'p')
127
			->set("is_valid", $qb->createParameter('is_valid'))
128
			->where('EXISTS (' . $sub->getSQL() . ')')
129
			->setParameter('image_id', $imageId)
130
			->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL)
131
			->execute();
132
	}
133
134
	/**
135
	 * Updates one face with $faceId to database to person ID $personId.
136
	 *
137
	 * @param int $faceId ID of the face
138
	 * @param int|null $personId ID of the person
139
	 */
140
	private function updateFace(int $faceId, $personId) {
141
		$qb = $this->db->getQueryBuilder();
142
		$qb->update('face_recognition_faces')
143
			->set("person", $qb->createNamedParameter($personId))
144
			->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
145
			->execute();
146
	}
147
148
	/**
149
	 * Based on current clusters and new clusters, do database reconciliation.
150
	 * It tries to do that in minumal number of SQL queries. Operation is atomic.
151
	 *
152
	 * Clusters are array, where keys are ID of persons, and values are indexed arrays
153
	 * with values that are ID of the faces for those persons.
154
	 *
155
	 * @param string $userId ID of the user that clusters belong to
156
	 * @param array $currentClusters Current clusters
157
	 * @param array $newClusters New clusters
158
	 */
159
	public function mergeClusterToDatabase(string $userId, $currentClusters, $newClusters) {
160
		$this->db->beginTransaction();
161
		$currentDateTime = new \DateTime();
162
163
		try {
164
			// Delete clusters that do not exist anymore
165
			foreach($currentClusters as $oldPerson => $oldFaces) {
166
				if (array_key_exists($oldPerson, $newClusters)) {
167
					continue;
168
				}
169
170
				// OK, we bumped into cluster that existed and now it does not exist.
171
				// We need to remove all references to it and to delete it.
172
				foreach ($oldFaces as $oldFace) {
173
					$this->updateFace($oldFace, null);
174
				}
175
176
				// todo: this is not very cool. What if user had associated linked user to this. And all lost?
177
				$qb = $this->db->getQueryBuilder();
178
				// todo: for extra safety, we should probably add here additional condition, where (user=$userId)
179
				$qb
180
					->delete($this->getTableName())
181
					->where($qb->expr()->eq('id', $qb->createNamedParameter($oldPerson)))
182
					->execute();
183
			}
184
185
			// Add or modify existing clusters
186
			foreach($newClusters as $newPerson=>$newFaces) {
187
				if (array_key_exists($newPerson, $currentClusters)) {
188
					// This cluster existed, check if faces match
189
					$oldFaces = $currentClusters[$newPerson];
190
					if ($newFaces == $oldFaces) {
191
						continue;
192
					}
193
194
					// OK, set of faces do differ. Now, we could potentially go into finer grain details
195
					// and add/remove each individual face, but this seems too detailed. Enough is to
196
					// reset all existing faces to null and to add new faces to new person. That should
197
					// take care of both faces that are removed from cluster, as well as for newly added
198
					// faces to this cluster.
199
					foreach ($oldFaces as $oldFace) {
200
						$this->updateFace($oldFace, null);
201
					}
202
					foreach ($newFaces as $newFace) {
203
						$this->updateFace($newFace, $newPerson);
204
					}
205
				} else {
206
					// This person doesn't even exist, insert it
207
					$qb = $this->db->getQueryBuilder();
208
					$qb
209
						->insert($this->getTableName())
210
						->values([
211
							'user' => $qb->createNamedParameter($userId),
212
							'name' => $qb->createNamedParameter(sprintf("New person %d", $newPerson)),
213
							'is_valid' => $qb->createNamedParameter(true),
214
							'last_generation_time' => $qb->createNamedParameter($currentDateTime, IQueryBuilder::PARAM_DATE),
215
							'linked_user' => $qb->createNamedParameter(null)])
216
						->execute();
217
					$insertedPersonId = $this->db->lastInsertId($this->getTableName());
218
					foreach ($newFaces as $newFace) {
219
						$this->updateFace($newFace, $insertedPersonId);
220
					}
221
				}
222
			}
223
224
			$this->db->commit();
225
		} catch (\Exception $e) {
226
			$this->db->rollBack();
227
			throw $e;
228
		}
229
	}
230
}
231