Completed
Push — master ( 8110a3...fe2a60 )
by Morris
18:07 queued 10s
created

UpdateCalendarResourcesRoomsBackgroundJob   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 319
Duplicated Lines 38.24 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 122
loc 319
rs 9.1199
c 0
b 0
f 0
wmc 41
lcom 1
cbo 9

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
A run() 0 4 1
C runResources() 48 48 11
C runRooms() 48 48 11
A getCached() 0 12 2
A getCachedResourceIds() 13 13 3
A getCachedRoomIds() 13 13 3
A sortByNewDeletedExisting() 0 22 4
A addToCache() 0 15 1
A deleteFromCache() 0 12 2
A updateCache() 0 13 1
A serializeGroupRestrictions() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like UpdateCalendarResourcesRoomsBackgroundJob often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UpdateCalendarResourcesRoomsBackgroundJob, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright 2018, Georg Ehrke <[email protected]>
4
 *
5
 * @author Georg Ehrke <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OCA\DAV\BackgroundJob;
25
26
use OC\BackgroundJob\TimedJob;
27
use OCA\DAV\CalDAV\CalDavBackend;
28
use OCP\Calendar\BackendTemporarilyUnavailableException;
29
use OCP\Calendar\Resource\IManager as IResourceManager;
30
use OCP\Calendar\Resource\IResource;
31
use OCP\Calendar\Room\IManager as IRoomManager;
32
use OCP\Calendar\Room\IRoom;
33
use OCP\IDBConnection;
34
35
class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob {
36
37
	/** @var IResourceManager */
38
	private $resourceManager;
39
40
	/** @var IRoomManager */
41
	private $roomManager;
42
43
	/** @var IDBConnection */
44
	private $db;
45
46
	/** @var CalDavBackend */
47
	private $calDavBackend;
48
49
	/** @var string */
50
	private $resourceDbTable;
51
52
	/** @var string */
53
	private $resourcePrincipalUri;
54
55
	/** @var string */
56
	private $roomDbTable;
57
58
	/** @var string */
59
	private $roomPrincipalUri;
60
61
	/**
62
	 * UpdateCalendarResourcesRoomsBackgroundJob constructor.
63
	 *
64
	 * @param IResourceManager $resourceManager
65
	 * @param IRoomManager $roomManager
66
	 * @param IDBConnection $dbConnection
67
	 * @param CalDavBackend $calDavBackend
68
	 */
69
	public function __construct(IResourceManager $resourceManager, IRoomManager $roomManager,
70
								IDBConnection $dbConnection, CalDavBackend $calDavBackend) {
71
		$this->resourceManager = $resourceManager;
72
		$this->roomManager = $roomManager;
73
		$this->db = $dbConnection;
74
		$this->calDavBackend = $calDavBackend;
75
		$this->resourceDbTable = 'calendar_resources';
76
		$this->resourcePrincipalUri = 'principals/calendar-resources';
77
		$this->roomDbTable = 'calendar_rooms';
78
		$this->roomPrincipalUri = 'principals/calendar-rooms';
79
80
		// run once an hour
81
		$this->setInterval(60 * 60);
82
	}
83
84
	/**
85
	 * @param $argument
86
	 */
87
	public function run($argument) {
88
		$this->runResources();
89
		$this->runRooms();
90
	}
91
92
	/**
93
	 * run timed job for resources
94
	 */
95 View Code Duplication
	private function runResources() {
96
		$resourceBackends = $this->resourceManager->getBackends();
97
		$cachedResources = $this->getCached($this->resourceDbTable);
98
		$cachedResourceIds = $this->getCachedResourceIds($cachedResources);
99
100
		$remoteResourceIds = [];
101
		foreach($resourceBackends as $resourceBackend) {
102
			try {
103
				$remoteResourceIds[$resourceBackend->getBackendIdentifier()] =
104
					$resourceBackend->listAllResources();
105
			} catch(BackendTemporarilyUnavailableException $ex) {
106
				// If the backend is temporarily unavailable
107
				// ignore this backend in this execution
108
				unset($cachedResourceIds[$resourceBackend->getBackendIdentifier()]);
109
			}
110
		}
111
112
		$sortedResources = $this->sortByNewDeletedExisting($cachedResourceIds, $remoteResourceIds);
113
114
		foreach($sortedResources['new'] as $backendId => $newResources) {
115
			foreach ($newResources as $newResource) {
116
				$backend = $this->resourceManager->getBackend($backendId);
117
				if ($backend === null) {
118
					continue;
119
				}
120
121
				$resource = $backend->getResource($newResource);
122
				$this->addToCache($this->resourceDbTable, $resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by $backend->getResource($newResource) on line 121 can be null; however, OCA\DAV\BackgroundJob\Up...groundJob::addToCache() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
123
			}
124
		}
125
		foreach($sortedResources['deleted'] as $backendId => $deletedResources) {
126
			foreach ($deletedResources as $deletedResource) {
127
				$this->deleteFromCache($this->resourceDbTable,
128
					$this->resourcePrincipalUri, $backendId, $deletedResource);
129
			}
130
		}
131
		foreach($sortedResources['edited'] as $backendId => $editedResources) {
132
			foreach ($editedResources as $editedResource) {
133
				$backend = $this->resourceManager->getBackend($backendId);
134
				if ($backend === null) {
135
					continue;
136
				}
137
138
				$resource = $backend->getResource($editedResource);
139
				$this->updateCache($this->resourceDbTable, $resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by $backend->getResource($editedResource) on line 138 can be null; however, OCA\DAV\BackgroundJob\Up...roundJob::updateCache() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
140
			}
141
		}
142
	}
143
144
	/**
145
	 * run timed job for rooms
146
	 */
147 View Code Duplication
	private function runRooms() {
148
		$roomBackends = $this->roomManager->getBackends();
149
		$cachedRooms = $this->getCached($this->roomDbTable);
150
		$cachedRoomIds = $this->getCachedRoomIds($cachedRooms);
151
152
		$remoteRoomIds = [];
153
		foreach($roomBackends as $roomBackend) {
154
			try {
155
				$remoteRoomIds[$roomBackend->getBackendIdentifier()] =
156
					$roomBackend->listAllRooms();
157
			} catch(BackendTemporarilyUnavailableException $ex) {
158
				// If the backend is temporarily unavailable
159
				// ignore this backend in this execution
160
				unset($cachedRoomIds[$roomBackend->getBackendIdentifier()]);
161
			}
162
		}
163
164
		$sortedRooms = $this->sortByNewDeletedExisting($cachedRoomIds, $remoteRoomIds);
165
166
		foreach($sortedRooms['new'] as $backendId => $newRooms) {
167
			foreach ($newRooms as $newRoom) {
168
				$backend = $this->roomManager->getBackend($backendId);
169
				if ($backend === null) {
170
					continue;
171
				}
172
173
				$resource = $backend->getRoom($newRoom);
174
				$this->addToCache($this->roomDbTable, $resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by $backend->getRoom($newRoom) on line 173 can be null; however, OCA\DAV\BackgroundJob\Up...groundJob::addToCache() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
175
			}
176
		}
177
		foreach($sortedRooms['deleted'] as $backendId => $deletedRooms) {
178
			foreach ($deletedRooms as $deletedRoom) {
179
				$this->deleteFromCache($this->roomDbTable,
180
					$this->roomPrincipalUri, $backendId, $deletedRoom);
181
			}
182
		}
183
		foreach($sortedRooms['edited'] as $backendId => $editedRooms) {
184
			foreach ($editedRooms as $editedRoom) {
185
				$backend = $this->roomManager->getBackend($backendId);
186
				if ($backend === null) {
187
					continue;
188
				}
189
190
				$resource = $backend->getRoom($editedRoom);
191
				$this->updateCache($this->roomDbTable, $resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by $backend->getRoom($editedRoom) on line 190 can be null; however, OCA\DAV\BackgroundJob\Up...roundJob::updateCache() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
192
			}
193
		}
194
	}
195
196
	/**
197
	 * get cached db rows for resources / rooms
198
	 * @param string $tableName
199
	 * @return array
200
	 */
201
	private function getCached($tableName):array {
202
		$query = $this->db->getQueryBuilder();
203
		$query->select('*')->from($tableName);
204
205
		$rows = [];
206
		$stmt = $query->execute();
207
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
208
			$rows[] = $row;
209
		}
210
211
		return $rows;
212
	}
213
214
	/**
215
	 * @param array $cachedResources
216
	 * @return array
217
	 */
218 View Code Duplication
	private function getCachedResourceIds(array $cachedResources):array {
219
		$cachedResourceIds = [];
220
		foreach ($cachedResources as $cachedResource) {
221
			if (!isset($cachedResourceIds[$cachedResource['backend_id']])) {
222
				$cachedResourceIds[$cachedResource['backend_id']] = [];
223
			}
224
225
			$cachedResourceIds[$cachedResource['backend_id']][] =
226
				$cachedResource['resource_id'];
227
		}
228
229
		return $cachedResourceIds;
230
	}
231
232
	/**
233
	 * @param array $cachedRooms
234
	 * @return array
235
	 */
236 View Code Duplication
	private function getCachedRoomIds(array $cachedRooms):array {
237
		$cachedRoomIds = [];
238
		foreach ($cachedRooms as $cachedRoom) {
239
			if (!isset($cachedRoomIds[$cachedRoom['backend_id']])) {
240
				$cachedRoomIds[$cachedRoom['backend_id']] = [];
241
			}
242
243
			$cachedRoomIds[$cachedRoom['backend_id']][] =
244
				$cachedRoom['resource_id'];
245
		}
246
247
		return $cachedRoomIds;
248
	}
249
250
	/**
251
	 * sort list of ids by whether they appear only in the backend /
252
	 * only in the cache / in both
253
	 *
254
	 * @param array $cached
255
	 * @param array $remote
256
	 * @return array
257
	 */
258
	private function sortByNewDeletedExisting(array $cached, array $remote):array {
259
		$sorted = [
260
			'new' => [],
261
			'deleted' => [],
262
			'edited' => [],
263
		];
264
265
		$backendIds = array_merge(array_keys($cached), array_keys($remote));
266
		foreach($backendIds as $backendId) {
267
			if (!isset($cached[$backendId])) {
268
				$sorted['new'][$backendId] = $remote[$backendId];
269
			} elseif (!isset($remote[$backendId])) {
270
				$sorted['deleted'][$backendId] = $cached[$backendId];
271
			} else {
272
				$sorted['new'][$backendId] = array_diff($remote[$backendId], $cached[$backendId]);
273
				$sorted['deleted'][$backendId] = array_diff($cached[$backendId], $remote[$backendId]);
274
				$sorted['edited'][$backendId] = array_intersect($remote[$backendId], $cached[$backendId]);
275
			}
276
		}
277
278
		return $sorted;
279
	}
280
281
	/**
282
	 * add entry to cache that exists remotely but not yet in cache
283
	 *
284
	 * @param string $table
285
	 * @param IResource|IRoom $remote
286
	 */
287
	private function addToCache($table, $remote) {
288
		$query = $this->db->getQueryBuilder();
289
		$query->insert($table)
290
			->values([
291
				'backend_id' => $query->createNamedParameter($remote->getBackend()->getBackendIdentifier()),
292
				'resource_id' => $query->createNamedParameter($remote->getId()),
293
				'email' => $query->createNamedParameter($remote->getEMail()),
294
				'displayname' => $query->createNamedParameter($remote->getDisplayName()),
295
				'group_restrictions' => $query->createNamedParameter(
296
					$this->serializeGroupRestrictions(
297
						$remote->getGroupRestrictions()
298
					))
299
			])
300
			->execute();
301
	}
302
303
	/**
304
	 * delete entry from cache that does not exist anymore remotely
305
	 *
306
	 * @param string $table
307
	 * @param string $principalUri
308
	 * @param string $backendId
309
	 * @param string $resourceId
310
	 */
311
	private function deleteFromCache($table, $principalUri, $backendId, $resourceId) {
312
		$query = $this->db->getQueryBuilder();
313
		$query->delete($table)
314
			->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
315
			->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)))
316
			->execute();
317
318
		$calendar = $this->calDavBackend->getCalendarByUri($principalUri, implode('-', [$backendId, $resourceId]));
319
		if ($calendar !== null) {
320
			$this->calDavBackend->deleteCalendar($calendar['id']);
321
		}
322
	}
323
324
	/**
325
	 * update an existing entry in cache
326
	 *
327
	 * @param string $table
328
	 * @param IResource|IRoom $remote
329
	 */
330
	private function updateCache($table, $remote) {
331
		$query = $this->db->getQueryBuilder();
332
		$query->update($table)
333
			->set('email', $query->createNamedParameter($remote->getEMail()))
334
			->set('displayname', $query->createNamedParameter($remote->getDisplayName()))
335
			->set('group_restrictions', $query->createNamedParameter(
336
				$this->serializeGroupRestrictions(
337
					$remote->getGroupRestrictions()
338
				)))
339
			->where($query->expr()->eq('backend_id', $query->createNamedParameter($remote->getBackend()->getBackendIdentifier())))
340
			->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($remote->getId())))
341
			->execute();
342
	}
343
344
	/**
345
	 * serialize array of group restrictions to store them in database
346
	 *
347
	 * @param array $groups
348
	 * @return string
349
	 */
350
	private function serializeGroupRestrictions(array $groups):string {
351
		return \json_encode($groups);
352
	}
353
}
354