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 classes like Configuration 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 Configuration, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
20 | class Configuration |
||
21 | { |
||
22 | const DETECT_CONTENT_HEADER = 1; |
||
23 | const DETECT_CONTENT_EXTENSION = 2; |
||
24 | const DETECT_CONTENT_PARAM = 3; |
||
25 | const EXPOSE_REQUEST_HEADER = 1; |
||
26 | const EXPOSE_REQUEST_PARAM = 2; |
||
27 | const EXPOSE_REQUEST_PARAM_GET = 3; |
||
28 | const EXPOSE_REQUEST_PARAM_POST = 4; |
||
29 | |||
30 | // any alteration will have an effect on drest-common (AbstractRepresentation.php) |
||
31 | public static $detectContentOptions = [ |
||
32 | self::DETECT_CONTENT_HEADER => 'Header', |
||
33 | self::DETECT_CONTENT_EXTENSION => 'Extension', |
||
34 | self::DETECT_CONTENT_PARAM => 'Parameter' |
||
35 | ]; |
||
36 | public static $exposeRequestOptions = [ |
||
37 | self::EXPOSE_REQUEST_HEADER => 'X-Expose', |
||
38 | self::EXPOSE_REQUEST_PARAM => 'Parameter', |
||
39 | self::EXPOSE_REQUEST_PARAM_GET => 'Get Parameter', |
||
40 | self::EXPOSE_REQUEST_PARAM_POST => 'Post Parameter' |
||
41 | ]; |
||
42 | /** |
||
43 | * Configuration attributes |
||
44 | * @var array |
||
45 | */ |
||
46 | protected $_attributes = []; |
||
47 | |||
48 | /** |
||
49 | * Set configuration defaults |
||
50 | */ |
||
51 | 47 | public function __construct() |
|
52 | { |
||
53 | // Turn off debug mode |
||
54 | 47 | $this->setDebugMode(false); |
|
55 | // Allow content detection using the Accept header |
||
56 | 47 | $this->setDetectContentOptions([self::DETECT_CONTENT_HEADER => 'Accept']); |
|
57 | // Use Json and XML as the default representations |
||
58 | // @todo: This probably should be registered in this way. Use a similar method as the adapter classes |
||
59 | 47 | $this->setDefaultRepresentations(array('Json', 'Xml')); |
|
60 | // Set the default method for retrieving class metadata. |
||
61 | 47 | $this->setMetadataDriverClass('\Drest\Mapping\Driver\AnnotationDriver'); |
|
62 | // register the default request adapter classes |
||
63 | 47 | $this->_attributes['requestAdapterClasses'] = []; |
|
64 | 47 | $this->registerRequestAdapterClasses(Request::$defaultAdapterClasses); |
|
65 | // register the default response adapter classes |
||
66 | 47 | $this->_attributes['responseAdapterClasses'] = []; |
|
67 | 47 | $this->registerResponseAdapterClasses(Response::$defaultAdapterClasses); |
|
68 | // Depth of exposure on entity fields => relations |
||
69 | 47 | $this->setExposureDepth(2); |
|
70 | // Don't follow any relation type |
||
71 | 47 | $this->setExposureRelationsFetchType(null); |
|
72 | // Don't set any expose request options |
||
73 | 47 | $this->setExposeRequestOptions([]); |
|
74 | // Allow OPTIONS request on resources |
||
75 | 47 | $this->setAllowOptionsRequest(true); |
|
76 | // Set the route base paths to be an empty array |
||
77 | 47 | $this->_attributes['routeBasePaths'] = []; |
|
78 | // Set the paths to the config files as an empty array |
||
79 | 47 | $this->_attributes['pathsToConfigFiles'] = []; |
|
80 | // Don't send a 415 if we don't match a representation class, default to first available one |
||
81 | 47 | $this->set415ForNoMediaMatch(false); |
|
82 | // Set the default error handler class (immutable) |
||
83 | 47 | $this->_attributes['defaultErrorHandlerClass'] = 'DrestCommon\\Error\\Handler\\DefaultHandler'; |
|
84 | 47 | } |
|
85 | |||
86 | /** |
||
87 | * Set the debug mode - when on all DrestExceptions are rethrown, |
||
88 | * otherwise 500 errors are returned from the REST service |
||
89 | * Should be switched off in production |
||
90 | * @param boolean $setting |
||
91 | */ |
||
92 | 47 | public function setDebugMode($setting) |
|
93 | { |
||
94 | 47 | $this->_attributes['debugMode'] = (bool) $setting; |
|
95 | 47 | } |
|
96 | |||
97 | /** |
||
98 | * Set the default representation classes to be used across the entire API. |
||
99 | * Any representations defined locally on a resource will take precedence |
||
100 | * @param string[] $representations |
||
101 | */ |
||
102 | 47 | public function setDefaultRepresentations(array $representations) |
|
103 | { |
||
104 | 47 | $this->_attributes['defaultRepresentations'] = $representations; |
|
105 | 47 | } |
|
106 | |||
107 | /** |
||
108 | * Sets the class name of the metadata driver for instantiation. |
||
109 | * |
||
110 | * @param string $driver |
||
111 | */ |
||
112 | 47 | public function setMetadataDriverClass($driver) { |
|
113 | 47 | $this->_attributes['metaDataDriver'] = $driver; |
|
114 | 47 | } |
|
115 | |||
116 | /** |
||
117 | * Returns the class name of the metadata driver. |
||
118 | * @return string The namespaced class name. |
||
119 | */ |
||
120 | 31 | public function getMetadataDriverClass() { |
|
121 | 31 | return $this->_attributes['metaDataDriver']; |
|
122 | } |
||
123 | |||
124 | |||
125 | /** |
||
126 | * Register an array of request adapter classes |
||
127 | * @param array $classes |
||
128 | */ |
||
129 | 47 | public function registerRequestAdapterClasses(array $classes) |
|
130 | { |
||
131 | 47 | foreach ($classes as $class) { |
|
132 | 47 | $this->registerRequestAdapterClass($class); |
|
133 | 47 | } |
|
134 | 47 | } |
|
135 | |||
136 | /** |
||
137 | * Register a class name to be used as a request adapter |
||
138 | * @param string $class |
||
139 | */ |
||
140 | 47 | public function registerRequestAdapterClass($class) |
|
141 | { |
||
142 | 47 | if ($this->containsRequestAdapterClass($class) === false) { |
|
143 | 47 | $this->_attributes['requestAdapterClasses'][] = $class; |
|
144 | 47 | } |
|
145 | 47 | } |
|
146 | |||
147 | /** |
||
148 | * Does this configuration contain a request adapter class by name |
||
149 | * @param string $className |
||
150 | * @return boolean|integer returns the offset position if it exists (can be zero, do type check) |
||
151 | */ |
||
152 | 47 | public function containsRequestAdapterClass($className) |
|
153 | { |
||
154 | 47 | return $this->searchForAdapterClass($className, 'requestAdapterClasses'); |
|
155 | } |
||
156 | |||
157 | /** |
||
158 | * Search for a request/response adapter class by name |
||
159 | * @param $className |
||
160 | * @param $adapterType |
||
161 | * @return bool|integer returns the offset position if it exists |
||
162 | */ |
||
163 | 47 | protected function searchForAdapterClass($className, $adapterType) |
|
164 | { |
||
165 | 47 | if (($offset = array_search($className, $this->_attributes[$adapterType])) !== false) { |
|
166 | 2 | return $offset; |
|
167 | } |
||
168 | 47 | return false; |
|
169 | } |
||
170 | |||
171 | /** |
||
172 | * Register an array of response adapter classes |
||
173 | * @param array $classes |
||
174 | */ |
||
175 | 47 | public function registerResponseAdapterClasses(array $classes) |
|
176 | { |
||
177 | 47 | foreach ($classes as $class) { |
|
178 | 47 | $this->registerResponseAdapterClass($class); |
|
179 | 47 | } |
|
180 | 47 | } |
|
181 | |||
182 | /** |
||
183 | * Register a class name to be used as a response adapter |
||
184 | * @param string $class |
||
185 | */ |
||
186 | 47 | public function registerResponseAdapterClass($class) |
|
187 | { |
||
188 | 47 | if ($this->containsResponseAdapterClass($class) === false) { |
|
189 | 47 | $this->_attributes['responseAdapterClasses'][] = $class; |
|
190 | 47 | } |
|
191 | 47 | } |
|
192 | |||
193 | /** |
||
194 | * Does this configuration contain a response adapter class by name |
||
195 | * @param string $className |
||
196 | * @return boolean|integer returns the offset position if it exists (can be zero, do type check) |
||
197 | */ |
||
198 | 47 | public function containsResponseAdapterClass($className) |
|
199 | { |
||
200 | 47 | return $this->searchForAdapterClass($className, 'responseAdapterClasses'); |
|
201 | } |
||
202 | |||
203 | /** |
||
204 | * Set the default depth of columns to expose to client |
||
205 | * @param integer $depth |
||
206 | */ |
||
207 | 47 | public function setExposureDepth($depth) |
|
208 | { |
||
209 | 47 | $this->_attributes['defaultExposureDepth'] = (int) $depth; |
|
210 | 47 | } |
|
211 | |||
212 | /** |
||
213 | * Set the exposure fields by following relations that have the a certain fetch type. |
||
214 | * This is useful if you only want to display fields that are loaded eagerly. |
||
215 | * eg ->setExposureRelationsFetchType(ORMClassMetaDataInfo::FETCH_EAGER) |
||
216 | * @param integer $fetch |
||
217 | * @throws DrestException |
||
218 | */ |
||
219 | 47 | public function setExposureRelationsFetchType($fetch) |
|
220 | { |
||
221 | switch ($fetch) { |
||
222 | 47 | case ORMClassMetaDataInfo::FETCH_EAGER: |
|
223 | 47 | case ORMClassMetaDataInfo::FETCH_LAZY: |
|
224 | 47 | case ORMClassMetaDataInfo::FETCH_EXTRA_LAZY: |
|
225 | 47 | case null: |
|
226 | 47 | $this->_attributes['defaultExposureRelationsFetchType'] = $fetch; |
|
227 | 47 | break; |
|
228 | 1 | default: |
|
229 | 1 | throw DrestException::invalidExposeRelationFetchType(); |
|
230 | 1 | } |
|
231 | 47 | } |
|
232 | |||
233 | /** |
||
234 | * A setting to generically allow OPTIONS requests across the entire API. |
||
235 | * This can be overridden by using the @Route\Metadata $allowOptions parameter |
||
236 | * @param boolean $value |
||
237 | */ |
||
238 | 47 | public function setAllowOptionsRequest($value) |
|
239 | { |
||
240 | 47 | $this->_attributes['allowOptionsRequest'] = (bool) $value; |
|
241 | 47 | } |
|
242 | |||
243 | /** |
||
244 | * When no content type is detected, the response will default to using the first available. |
||
245 | * To switch this feature off and send a 415 error, call this configuration function |
||
246 | * @param boolean $value |
||
247 | */ |
||
248 | 47 | public function set415ForNoMediaMatch($value = true) |
|
249 | { |
||
250 | 47 | $this->_attributes['send415ForNoMediaMatch'] = (bool) $value; |
|
251 | 47 | } |
|
252 | |||
253 | /** |
||
254 | * Sets the cache driver implementation that is used for metadata caching. |
||
255 | * |
||
256 | * @param \Doctrine\Common\Cache\Cache $cacheImpl |
||
257 | */ |
||
258 | 33 | public function setMetadataCacheImpl(Cache $cacheImpl) |
|
259 | { |
||
260 | 33 | $this->_attributes['metadataCacheImpl'] = $cacheImpl; |
|
261 | 33 | } |
|
262 | |||
263 | /** |
||
264 | * Set a content option for detecting the media type to be used. To unset pass null as a value |
||
265 | * For any options that don't required a value, set them to true to activate them |
||
266 | * @param integer $option |
||
267 | * @param string $value |
||
268 | * @throws DrestException |
||
269 | */ |
||
270 | 47 | public function setDetectContentOption($option, $value) |
|
271 | { |
||
272 | 47 | if (array_key_exists($option, self::$detectContentOptions)) { |
|
273 | 47 | $this->_attributes['detectContentOptions'][$option] = $value; |
|
274 | 47 | } else { |
|
275 | 1 | throw DrestException::unknownDetectContentOption(); |
|
276 | } |
||
277 | 47 | } |
|
278 | |||
279 | /** |
||
280 | * Get detect content options. Returns an array indexed using constants as array key |
||
281 | * (value will be the value to be used for the content options) |
||
282 | * Eg array(self::DETECT_CONTENT_HEADER => 'Accept') |
||
283 | * @return array |
||
284 | */ |
||
285 | 27 | public function getDetectContentOptions() |
|
286 | { |
||
287 | 27 | return $this->_attributes['detectContentOptions']; |
|
288 | } |
||
289 | |||
290 | /** |
||
291 | * Set the methods to be used for detecting content type to be used to pull requests, overwrites previous settings |
||
292 | * Eg ->setDetectContentOptions(array(self::DETECT_CONTENT_HEADER => $headerName)) |
||
293 | * self::DETECT_CONTENT_HEADER = Uses the a header to detect the required content (typically use Accept) |
||
294 | * self::DETECT_CONTENT_EXTENSION = Uses an extension on the url eg .xml |
||
295 | * self::DETECT_CONTENT_PARAM = Uses a the "format" parameter |
||
296 | * @param array - pass either a single array value using the constant value as a key, or a multi-dimensional array. |
||
297 | */ |
||
298 | 47 | public function setDetectContentOptions(array $options) |
|
299 | { |
||
300 | 47 | $this->_attributes['detectContentOptions'] = []; |
|
301 | 47 | foreach ($options as $key => $value) { |
|
302 | 47 | $this->setDetectContentOption($key, $value); |
|
303 | 47 | } |
|
304 | 47 | } |
|
305 | |||
306 | /** |
||
307 | * Get the 415 for no representation match setting |
||
308 | * @return boolean |
||
309 | */ |
||
310 | 4 | public function get415ForNoMediaMatchSetting() |
|
311 | { |
||
312 | 4 | return $this->_attributes['send415ForNoMediaMatch']; |
|
313 | } |
||
314 | |||
315 | /** |
||
316 | * Method used to retrieve the required expose contents from the client. To unset pass null as value |
||
317 | * @param integer $option |
||
318 | * @param string $value |
||
319 | * @throws DrestException |
||
320 | */ |
||
321 | 2 | public function setExposeRequestOption($option, $value) |
|
322 | { |
||
323 | 2 | if (array_key_exists($option, self::$exposeRequestOptions)) { |
|
324 | 1 | $this->_attributes['exposeRequestOptions'][$option] = $value; |
|
325 | 1 | } else { |
|
326 | 1 | throw DrestException::unknownExposeRequestOption(); |
|
327 | } |
||
328 | 1 | } |
|
329 | |||
330 | /** |
||
331 | * Get the expose request options |
||
332 | * @return array $options |
||
333 | */ |
||
334 | 19 | public function getExposeRequestOptions() |
|
335 | { |
||
336 | 19 | return $this->_attributes['exposeRequestOptions']; |
|
337 | } |
||
338 | |||
339 | /** |
||
340 | * Set the methods to be used for detecting the expose content from the client. Overwrites any previous value |
||
341 | * Eg ->setExposeRequestOptions(array(self::EXPOSE_REQUEST_HEADER => $headerName)) |
||
342 | * @param array $options |
||
343 | */ |
||
344 | 47 | public function setExposeRequestOptions(array $options) |
|
345 | { |
||
346 | 47 | $this->_attributes['exposeRequestOptions'] = []; |
|
347 | 47 | foreach ($options as $key => $value) { |
|
348 | 2 | $this->setExposeRequestOption($key, $value); |
|
349 | 47 | } |
|
350 | 47 | } |
|
351 | |||
352 | /** |
||
353 | * Get the default exposure depth |
||
354 | * @return integer $depth |
||
355 | */ |
||
356 | 22 | public function getExposureDepth() |
|
360 | |||
361 | /** |
||
362 | * Gets the configured expose relations fetch type - returns null if not set |
||
363 | * @return integer|null $result |
||
364 | */ |
||
365 | 23 | public function getExposureRelationsFetchType() |
|
366 | { |
||
367 | 23 | if (isset($this->_attributes['defaultExposureRelationsFetchType'])) { |
|
368 | 1 | return $this->_attributes['defaultExposureRelationsFetchType']; |
|
369 | } |
||
370 | |||
371 | 22 | return null; |
|
372 | } |
||
373 | |||
374 | /** |
||
375 | * Un-register an adapter class name entry |
||
376 | * @param string $class |
||
377 | */ |
||
378 | 1 | public function unregisterResponseAdapterClass($class) |
|
379 | { |
||
380 | 1 | if (($offset = $this->containsResponseAdapterClass($class)) !== false) { |
|
381 | 1 | unset($this->_attributes['responseAdapterClasses'][$offset]); |
|
382 | 1 | } |
|
383 | 1 | } |
|
384 | |||
385 | /** |
||
386 | * get the registered response adapted classes |
||
387 | * @return array $class_name - a string array of class names |
||
388 | */ |
||
389 | 32 | public function getRegisteredResponseAdapterClasses() |
|
390 | { |
||
391 | 32 | return $this->_attributes['responseAdapterClasses']; |
|
392 | } |
||
393 | |||
394 | /** |
||
395 | * Un-register an adapter class name entry |
||
396 | * @param string $class |
||
397 | */ |
||
398 | 1 | public function unregisterRequestAdapterClass($class) |
|
399 | { |
||
400 | 1 | if (($offset = $this->containsRequestAdapterClass($class)) !== false) { |
|
401 | 1 | unset($this->_attributes['requestAdapterClasses'][$offset]); |
|
402 | 1 | } |
|
403 | 1 | } |
|
404 | |||
405 | /** |
||
406 | * get the registered request adapted classes |
||
407 | * @return array $class_name - a string array of class names |
||
408 | */ |
||
409 | 32 | public function getRegisteredRequestAdapterClasses() |
|
410 | { |
||
411 | 32 | return $this->_attributes['requestAdapterClasses']; |
|
412 | } |
||
413 | |||
414 | /** |
||
415 | * Are we globally allowing OPTIONS requests across all routes |
||
416 | * @return boolean $value |
||
417 | */ |
||
418 | 2 | public function getAllowOptionsRequest() |
|
419 | { |
||
420 | 2 | return $this->_attributes['allowOptionsRequest']; |
|
421 | } |
||
422 | |||
423 | /** |
||
424 | * Register paths to your configuration files. This will typically be where your entities live |
||
425 | * This will overwrite any previously registered paths. To add new one use addPathsToConfigFiles($paths) |
||
426 | * @param array $paths |
||
427 | */ |
||
428 | 31 | public function addPathsToConfigFiles($paths = []) |
|
429 | { |
||
430 | 31 | if (!isset($this->_attributes['pathsToConfigFiles'])) { |
|
431 | $this->_attributes['pathsToConfigFiles'] = []; |
||
432 | } |
||
433 | 31 | $this->_attributes['pathsToConfigFiles'] = array_merge($this->_attributes['pathsToConfigFiles'], (array) $paths); |
|
434 | 31 | } |
|
435 | |||
436 | /** |
||
437 | * Remove all the registered paths to config files, or just a specific entry $path |
||
438 | * @param string $path |
||
439 | */ |
||
440 | public function removePathsToConfigFiles($path = null) |
||
441 | { |
||
442 | if (is_null($path)) { |
||
443 | $this->_attributes['pathsToConfigFiles'] = []; |
||
444 | View Code Duplication | } else { |
|
445 | if (($offset = array_search($path, $this->_attributes['pathsToConfigFiles'])) !== false) |
||
446 | { |
||
447 | unset($this->_attributes['pathsToConfigFiles'][$offset]); |
||
448 | } |
||
449 | } |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * Get the paths to the drest configuration files |
||
454 | * @return array $paths |
||
455 | */ |
||
456 | 31 | public function getPathsToConfigFiles() |
|
457 | { |
||
458 | 31 | return $this->_attributes['pathsToConfigFiles']; |
|
459 | } |
||
460 | |||
461 | /** |
||
462 | * Add a base path to be used when matching routes. Eg /v1 would be useful IF you want versioning in the URL |
||
463 | * @param string $basePath |
||
464 | * @throws DrestException |
||
465 | */ |
||
466 | 3 | public function addRouteBasePath($basePath) |
|
473 | |||
474 | /** |
||
475 | * Remove a route base path (if it has been registered) |
||
476 | * @param string $basePath |
||
477 | * @return boolean true if $basePath was unset |
||
478 | */ |
||
479 | 1 | public function removeRouteBasePath($basePath) |
|
480 | { |
||
481 | 1 | $basePath = trim($basePath, '/'); |
|
482 | 1 | if (!is_string($basePath)) { |
|
483 | return false; |
||
484 | } |
||
485 | 1 | View Code Duplication | if (($offset = array_search($basePath, $this->_attributes['routeBasePaths'])) !== false) { |
486 | 1 | unset($this->_attributes['routeBasePaths'][$offset]); |
|
487 | |||
488 | 1 | return true; |
|
489 | } |
||
490 | |||
491 | return false; |
||
492 | } |
||
493 | |||
494 | /** |
||
495 | * Have base paths been registered - or look for a specific entry |
||
496 | * @param string $basePath - optional, has a specific route path been registered |
||
497 | * @return boolean true if route base paths have been registered |
||
498 | */ |
||
499 | 28 | public function hasRouteBasePaths($basePath = null) |
|
500 | { |
||
501 | 28 | if (!is_null($basePath)) { |
|
502 | $basePath = trim($basePath, '/'); |
||
503 | |||
504 | return in_array($basePath, $this->_attributes['routeBasePaths']); |
||
505 | } |
||
506 | |||
507 | 28 | return (sizeof($this->_attributes['routeBasePaths']) > 0) ? true : false; |
|
508 | } |
||
509 | |||
510 | /** |
||
511 | * Get all registered base path or a specific entry |
||
512 | * @return array $basePaths |
||
513 | */ |
||
514 | 2 | public function getRouteBasePaths() |
|
515 | { |
||
516 | 2 | return (array) $this->_attributes['routeBasePaths']; |
|
517 | } |
||
518 | |||
519 | /** |
||
520 | * Get the default representation classes to be used across the entire API |
||
521 | * @return array representation classes |
||
522 | */ |
||
523 | 2 | public function getDefaultRepresentations() |
|
524 | { |
||
525 | 2 | return (array) $this->_attributes['defaultRepresentations']; |
|
526 | } |
||
527 | |||
528 | /** |
||
529 | * Get the default error handler class |
||
530 | * @return string $className |
||
531 | */ |
||
532 | 26 | public function getDefaultErrorHandlerClass() |
|
536 | |||
537 | /** |
||
538 | * Ensures that this Configuration instance contains settings that are |
||
539 | * suitable for a production environment. |
||
540 | * |
||
541 | * @throws DrestException If a configuration setting has a value that is not suitable for a production. |
||
542 | */ |
||
543 | 2 | public function ensureProductionSettings() |
|
544 | { |
||
545 | 2 | if ($this->inDebugMode()) { |
|
546 | 1 | throw DrestException::currentlyRunningDebugMode(); |
|
547 | } |
||
548 | |||
549 | 1 | if (!$this->getMetadataCacheImpl()) { |
|
550 | 1 | throw DrestException::metadataCacheNotConfigured(); |
|
551 | } |
||
552 | } |
||
553 | |||
554 | /** |
||
555 | * Are we in debug mode? |
||
556 | * @return boolean |
||
557 | */ |
||
558 | 7 | public function inDebugMode() |
|
559 | { |
||
560 | 7 | return $this->_attributes['debugMode']; |
|
561 | } |
||
562 | |||
563 | /** |
||
564 | * Gets the cache driver implementation that is used for metadata caching. |
||
565 | * |
||
566 | * @return \Doctrine\Common\Cache\Cache |
||
567 | */ |
||
568 | 33 | public function getMetadataCacheImpl() |
|
569 | { |
||
570 | 33 | return isset($this->_attributes['metadataCacheImpl']) |
|
571 | 33 | ? $this->_attributes['metadataCacheImpl'] |
|
572 | 33 | : null; |
|
573 | } |
||
574 | } |
||
575 |