Passed
Push — master ( a7b3d9...78560e )
by Fran
03:42
created

DocumentorService::swaggerResponses()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 44
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 34
c 0
b 0
f 0
nc 6
nop 0
dl 0
loc 44
ccs 0
cts 34
cp 0
crap 42
rs 8.439
1
<?php
2
namespace PSFS\services;
3
4
use Propel\Runtime\Map\TableMap;
5
use PSFS\base\config\Config;
6
use PSFS\base\Logger;
7
use PSFS\base\Router;
8
use PSFS\base\Service;
9
use PSFS\base\types\helpers\GeneratorHelper;
10
use PSFS\base\types\helpers\InjectorHelper;
11
use PSFS\base\types\helpers\RouterHelper;
12
use Symfony\Component\Finder\Finder;
13
14
/**
15
 * Class DocumentorService
16
 * @package PSFS\services
17
 */
18
class DocumentorService extends Service
19
{
20
    const DTO_INTERFACE = '\\PSFS\\base\\dto\\Dto';
21
    const MODEL_INTERFACE = '\\Propel\\Runtime\\ActiveRecord\\ActiveRecordInterface';
22
    /**
23
     * @Inyectable
24
     * @var \PSFS\base\Router route
25
     */
26
    protected $route;
27
28
    /**
29
     * Method that extract all modules
30
     * @param string $requestModule
31
     * @return array
32
     */
33
    public function getModules($requestModule)
34
    {
35
        $modules = [];
36
        $domains = $this->route->getDomains();
37
        if (count($domains)) {
38
            foreach ($domains as $module => $info) {
39
                try {
40
                    $module = preg_replace('/(@|\/)/', '', $module);
41
                    if (!preg_match('/^ROOT/i', $module) && $module == $requestModule) {
42
                        $modules = [
43
                            'name' => $module,
44
                            'path' => realpath($info['template'] . DIRECTORY_SEPARATOR . '..'),
45
                        ];
46
                    }
47
                } catch (\Exception $e) {
48
                    $modules[] = $e->getMessage();
49
                }
50
            }
51
        }
52
53
        return $modules;
54
    }
55
56
    /**
57
     * Method that extract all endpoints for each module
58
     *
59
     * @param array $module
60
     *
61
     * @return array
62
     */
63
    public function extractApiEndpoints(array $module)
64
    {
65
        $module_path = $module['path'] . DIRECTORY_SEPARATOR . 'Api';
66
        $module_name = $module['name'];
67
        $endpoints = [];
68
        if (file_exists($module_path)) {
69
            $finder = new Finder();
70
            $finder->files()->in($module_path)->depth(0)->name('*.php');
71
            if (count($finder)) {
72
                /** @var \SplFileInfo $file */
73
                foreach ($finder as $file) {
74
                    $namespace = "\\{$module_name}\\Api\\" . str_replace('.php', '', $file->getFilename());
75
                    $info = $this->extractApiInfo($namespace, $module_name);
76
                    if (!empty($info)) {
77
                        $endpoints[$namespace] = $info;
78
                    }
79
                }
80
            }
81
        }
82
        return $endpoints;
83
    }
84
85
    /**
86
     * Method that extract all the endpoit information by reflection
87
     *
88
     * @param string $namespace
89
     *
90
     * @return array
91
     */
92
    public function extractApiInfo($namespace, $module)
93
    {
94
        $info = [];
95
        if (class_exists($namespace)) {
96
            $reflection = new \ReflectionClass($namespace);
97
            foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
98
                try {
99
                    $mInfo = $this->extractMethodInfo($namespace, $method, $reflection, $module);
100
                    if (NULL !== $mInfo) {
101
                        $info[] = $mInfo;
102
                    }
103
                } catch (\Exception $e) {
104
                    Logger::getInstance()->errorLog($e->getMessage());
105
                }
106
            }
107
        }
108
        return $info;
109
    }
110
111
    /**
112
     * Extract route from doc comments
113
     *
114
     * @param string $comments
115
     *
116
     * @return string
117
     */
118
    protected function extractRoute($comments = '')
119
    {
120
        $route = '';
121
        preg_match('/@route\ (.*)\n/i', $comments, $route);
122
123
        return $route[1];
124
    }
125
126
    /**
127
     * Extract api from doc comments
128
     *
129
     * @param string $comments
130
     *
131
     * @return string
132
     */
133
    protected function extractApi($comments = '')
134
    {
135
        $api = '';
136
        preg_match('/@api\ (.*)\n/i', $comments, $api);
137
138
        return $api[1];
139
    }
140
141
    /**
142
     * Extract api from doc comments
143
     *
144
     * @param string $comments
145
     *
146
     * @return boolean
147
     */
148
    protected function checkDeprecated($comments = '')
149
    {
150
        return false != preg_match('/@deprecated\n/i', $comments);
151
    }
152
153
    /**
154
     * Extract visibility from doc comments
155
     *
156
     * @param string $comments
157
     *
158
     * @return boolean
159
     */
160
    protected function extractVisibility($comments = '')
161
    {
162
        $visible = TRUE;
163
        preg_match('/@visible\ (true|false)\n/i', $comments, $visibility);
164
        if (count($visibility)) {
165
            $visible = !('false' == $visibility[1]);
166
        }
167
168
        return $visible;
169
    }
170
171
    /**
172
     * Method that extract the description for the endpoint
173
     *
174
     * @param string $comments
175
     *
176
     * @return string
177
     */
178
    protected function extractDescription($comments = '')
179
    {
180
        $description = '';
181
        $docs = explode("\n", $comments);
182
        if (count($docs)) {
183
            foreach ($docs as &$doc) {
184 View Code Duplication
                if (!preg_match('/(\*\*|\@)/i', $doc) && preg_match('/\*\ /i', $doc)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
                    $doc = explode('* ', $doc);
186
                    $description = $doc[1];
187
                }
188
            }
189
        }
190
191
        return $description;
192
    }
193
194
    /**
195
     * Method that extract the type of a variable
196
     *
197
     * @param string $comments
198
     *
199
     * @return string
200
     */
201
    public static function extractVarType($comments = '')
202
    {
203
        $type = 'string';
204
        preg_match('/@var\ (.*) (.*)\n/i', $comments, $varType);
205
        if (count($varType)) {
206
            $aux = trim($varType[1]);
207
            $type = str_replace(' ', '', strlen($aux) > 0 ? $varType[1] : $varType[2]);
208
        }
209
210
        return $type;
211
    }
212
213
    /**
214
     * Method that extract the payload for the endpoint
215
     *
216
     * @param string $model
217
     * @param string $comments
218
     *
219
     * @return array
220
     */
221
    protected function extractPayload($model, $comments = '')
222
    {
223
        $payload = [];
224
        preg_match('/@payload\ (.*)\n/i', $comments, $doc);
225
        if (count($doc)) {
226
            $namespace = str_replace('{__API__}', $model, $doc[1]);
227
            $payload = $this->extractModelFields($namespace);
228
        }
229
230
        return $payload;
231
    }
232
233
    /**
234
     * Extract all the properties from Dto class
235
     *
236
     * @param string $class
237
     *
238
     * @return array
239
     */
240
    protected function extractDtoProperties($class)
241
    {
242
        $properties = [];
243
        $reflector = new \ReflectionClass($class);
244
        if ($reflector->isSubclassOf(self::DTO_INTERFACE)) {
245
            $properties = array_merge($properties, InjectorHelper::extractVariables($reflector, \ReflectionMethod::IS_PUBLIC));
0 ignored issues
show
Unused Code introduced by
The call to InjectorHelper::extractVariables() has too many arguments starting with \ReflectionMethod::IS_PUBLIC.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
246
        }
247
248
        return $properties;
249
    }
250
251
    /**
252
     * Extract return class for api endpoint
253
     *
254
     * @param string $model
255
     * @param string $comments
256
     *
257
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
258
     */
259
    protected function extractReturn($model, $comments = '')
260
    {
261
        $modelDto = [];
262
        preg_match('/\@return\ (.*)\((.*)\)\n/i', $comments, $returnTypes);
263
        if (count($returnTypes)) {
264
            // Extract principal DTO information
265
            if (array_key_exists(1, $returnTypes)) {
266
                $modelDto = $this->extractDtoProperties($returnTypes[1]);
267
            }
268
            if (array_key_exists(2, $returnTypes)) {
269
                $subDtos = preg_split('/,?\ /', str_replace('{__API__}', $model, $returnTypes[2]));
270
                if (count($subDtos)) {
271
                    foreach ($subDtos as $subDto) {
272
                        $isArray = false;
273
                        list($field, $dtoName) = explode('=', $subDto);
274
                        if (false !== strpos($dtoName, '[') && false !== strpos($dtoName, ']')) {
275
                            $dtoName = str_replace(']', '', str_replace('[', '', $dtoName));
276
                            $isArray = true;
277
                        }
278
                        $dto = $this->extractModelFields($dtoName);
279
                        $modelDto[$field] = ($isArray) ? [$dto] : $dto;
280
                        $modelDto['objects'][$dtoName] = $dto;
281
                    }
282
                }
283
            }
284
        }
285
286
        return $modelDto;
287
    }
288
289
    /**
290
     * Extract all fields from a ActiveResource model
291
     *
292
     * @param string $namespace
293
     *
294
     * @return mixed
295
     */
296
    protected function extractModelFields($namespace)
297
    {
298
        $payload = [];
299
        try {
300
            $reflector = new \ReflectionClass($namespace);
301
            // Checks if reflector is a subclass of propel ActiveRecords
302
            if (NULL !== $reflector && $reflector->isSubclassOf(self::MODEL_INTERFACE)) {
303
                $tableMap = $namespace::TABLE_MAP;
304
                $fieldNames = $tableMap::getFieldNames(TableMap::TYPE_FIELDNAME);
305
                if (count($fieldNames)) {
306
                    foreach ($fieldNames as $field) {
307
                        $variable = $reflector->getProperty(strtolower($field));
308
                        $varDoc = $variable->getDocComment();
309
                        $payload[$tableMap::translateFieldName($field, TableMap::TYPE_FIELDNAME, TableMap::TYPE_PHPNAME)] = $this->extractVarType($varDoc);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 155 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
310
                    }
311
                }
312
            } elseif (null !== $reflector && $reflector->isSubclassOf(self::DTO_INTERFACE)) {
313
                $payload = $this->extractDtoProperties($namespace);
314
            }
315
        } catch (\Exception $e) {
316
            Logger::getInstance()->errorLog($e->getMessage());
317
        }
318
319
        return $payload;
320
    }
321
322
    /**
323
     * Method that extract all the needed info for each method in each API
324
     *
325
     * @param string $namespace
326
     * @param \ReflectionMethod $method
327
     * @param \ReflectionClass $reflection
328
     * @param string $module
329
     *
330
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be null|array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
331
     */
332
    protected function extractMethodInfo($namespace, \ReflectionMethod $method, \ReflectionClass $reflection, $module)
333
    {
334
        $methodInfo = NULL;
335
        $docComments = $method->getDocComment();
336
        if (FALSE !== $docComments && preg_match('/\@route\ /i', $docComments)) {
337
            $api = self::extractApi($reflection->getDocComment());
338
            list($route, $info) = RouterHelper::extractRouteInfo($method, $api, $module);
339
            $route = explode('#|#', $route);
340
            $modelNamespace = str_replace('Api', 'Models', $namespace);
341
            if ($info['visible'] && !self::checkDeprecated($docComments)) {
342
                try {
343
                    $return = $this->extractReturn($modelNamespace, $docComments);
344
                    $methodInfo = [
345
                        'url' => array_pop($route),
346
                        'method' => $info['http'],
347
                        'description' => $info['label'],
348
                        'return' => $return,
349
                        'objects' => $return['objects'],
350
                    ];
351
                    unset($methodInfo['return']['objects']);
352
                    if (in_array($methodInfo['method'], ['POST', 'PUT'])) {
353
                        $methodInfo['payload'] = $this->extractPayload($modelNamespace, $docComments);
354
                    } elseif($method->getNumberOfParameters() > 0) {
355
                        $methodInfo['parameters'] = [];
356
                        foreach($method->getParameters() as $parameter) {
357
                            $parameterName = $parameter->getName();
358
                            $types = [];
359
                            preg_match_all('/\@param\ (.*)\ \$'.$parameterName.'$/im', $docComments, $types);
360
                            if(count($types) > 1) {
361
                                $methodInfo['parameters'][$parameterName] = $types[1][0];
362
                            }
363
                        }
364
                    }
365
                } catch (\Exception $e) {
366
                    jpre($e->getMessage());
367
                    Logger::getInstance()->errorLog($e->getMessage());
368
                }
369
            }
370
        }
371
372
        return $methodInfo;
373
    }
374
375
    /**
376
     * Translator from php types to swagger types
377
     * @param string $format
378
     *
379
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
380
     */
381
    public static function translateSwaggerFormats($format)
382
    {
383
        switch (strtolower($format)) {
384
            case 'bool':
385
            case 'boolean':
386
                $swaggerType = 'boolean';
387
                $swaggerFormat = '';
388
                break;
389
            default:
390
            case 'string':
391
            case 'varchar':
392
                $swaggerType = 'string';
393
                $swaggerFormat = '';
394
                break;
395
            case 'binary':
396
            case 'varbinary':
397
                $swaggerType = 'string';
398
                $swaggerFormat = 'binary';
399
                break;
400
            case 'int':
401
            case 'integer':
402
                $swaggerType = 'integer';
403
                $swaggerFormat = 'int32';
404
                break;
405
            case 'float':
406
            case 'double':
407
                $swaggerType = 'number';
408
                $swaggerFormat = strtolower($format);
409
                break;
410
            case 'date':
411
                $swaggerType = 'string';
412
                $swaggerFormat = 'date';
413
                break;
414
            case 'datetime':
415
                $swaggerType = 'string';
416
                $swaggerFormat = 'date-time';
417
                break;
418
419
        }
420
        return [$swaggerType, $swaggerFormat];
421
    }
422
423
    /**
424
     * Method that parse the definitions for the api's
425
     * @param string $name
426
     * @param array $fields
427
     *
428
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array<string,string|array>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
429
     */
430
    public static function extractSwaggerDefinition($name, array $fields)
431
    {
432
        $definition = [
433
            $name => [
434
                "type" => "object",
435
                "properties" => [],
436
            ],
437
        ];
438
        foreach ($fields as $field => $format) {
439
            if (is_array($format)) {
440
                $subDtoName = preg_replace('/Dto$/', '', $field);
441
                $subDtoName = preg_replace('/DtoList$/', '', $subDtoName);
442
                $subDto = self::extractSwaggerDefinition($$subDtoName, ['definitions' => [
443
                    $subDtoName => $format,
444
                ]]);
445
                if (array_key_exists($subDtoName, $subDto)) {
446
                    $definitions = $subDto;
447
                } else {
448
                    $definitions[$subDtoName] = $subDto;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$definitions was never initialized. Although not strictly required by PHP, it is generally a good practice to add $definitions = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
449
                }
450
                $definition[$name]['properties'][$field] = [
451
                    '$ref' => "#/definitions/" . $subDtoName,
452
                ];
453
            } else {
454
                list($type, $format) = self::translateSwaggerFormats($format);
455
                $dto['properties'][$field] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$dto was never initialized. Although not strictly required by PHP, it is generally a good practice to add $dto = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
456
                    "type" => $type,
457
                ];
458
                $definition[$name]['properties'][$field] = [
459
                    "type" => $type,
460
                ];
461
                if (strlen($format)) {
462
                    $definition[$name]['properties'][$field]['format'] = $format;
463
                }
464
            }
465
        }
466
        return $definition;
467
    }
468
469
    /**
470
     * @return array
471
     */
472
    private static function swaggerResponses() {
473
        $codes = [200, 400, 404, 500];
474
        $responses = [];
475
        foreach($codes as $code) {
476
            switch($code) {
477
                default:
478
                case 200:
479
                    $message = _('Successful response');
480
                    break;
481
                case 400:
482
                    $message = _('Client error in request');
483
                    break;
484
                case 404:
485
                    $message = _('Service not found');
486
                    break;
487
                case 500:
488
                    $message = _('Server error');
489
                    break;
490
            }
491
            $responses[$code] = [
492
                'description' => $message,
493
                'schema' => [
494
                    'type' => 'object',
495
                    'properties' => [
496
                        'success' => [
497
                            'type' => 'boolean'
498
                        ],
499
                        'data' => [
500
                            'type' => 'boolean',
501
                        ],
502
                        'total' => [
503
                            'type' => 'integer',
504
                            'format' => 'int32',
505
                        ],
506
                        'pages' => [
507
                            'type' => 'integer',
508
                            'format' => 'int32',
509
                        ]
510
                    ]
511
                ]
512
            ];
513
        }
514
        return $responses;
515
    }
516
517
    /**
518
     * Method that export
519
     * @param array $module
520
     *
521
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string|array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
522
     */
523
    public static function swaggerFormatter(array $module)
524
    {
525
        $formatted = [
526
            "swagger" => "2.0",
527
            "host" => preg_replace('/^(http|https)\:\/\//i', '', Router::getInstance()->getRoute('', true)) . $module['name'] . '/api',
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 135 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
528
            "schemes" => ["http", "https"],
529
            "info" => [
530
                "title" => _('Documentación API módulo ') . $module['name'],
531
                "version" => Config::getParam('api.version', '1.0'),
532
                "contact" => [
533
                    "name" => Config::getParam("author", "Fran López"),
534
                    "email" => Config::getParam("author_email", "[email protected]"),
535
                ]
536
            ]
537
        ];
538
        $dtos = $paths = [];
539
        $endpoints = DocumentorService::getInstance()->extractApiEndpoints($module);
540
        foreach ($endpoints as $model) {
541
            foreach ($model as $endpoint) {
542
                if(!preg_match('/^\/admin\//i', $endpoint['url']) && strlen($endpoint['url'])) {
543
                    $url = preg_replace('/\/'.$module['name'].'\/api/i', '', $endpoint['url']);
544
                    $description = $endpoint['description'];
545
                    $method = strtolower($endpoint['method']);
546
                    $paths[$url][$method] = [
547
                        'summary' => $description,
548
                        'produces' => ['application/json'],
549
                        'consumes' => ['application/json'],
550
                        'responses' => self::swaggerResponses(),
551
                        'parameters' => [],
552
                    ];
553
                    if(array_key_exists('parameters', $endpoint)) {
554
                        foreach($endpoint['parameters'] as $parameter => $type) {
555
                            list($type, $format) = self::translateSwaggerFormats($type);
556
                            $paths[$url][$method]['parameters'][] = [
557
                                'in' => 'path',
558
                                'required' => true,
559
                                'name' => $parameter,
560
                                'type' => $type,
561
                                'format' => $format,
562
                            ];
563
                        }
564
565
                    }
566
                    foreach($endpoint['objects'] as $name => $object) {
567
                        if(class_exists($name)) {
568
                            $class = GeneratorHelper::extractClassFromNamespace($name);
569
                            $classDefinition = [
570
                                'type' => 'object',
571
                                '$ref' => '#/definitions/' . $class,
572
                            ];
573
                            $paths[$url][$method]['responses'][200]['schema']['properties']['data'] = $classDefinition;
574
                            $dtos += self::extractSwaggerDefinition($class, $object);
575
                            if(!isset($paths[$url][$method]['tags']) || !in_array($class, $paths[$url][$method]['tags'])) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 123 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
576
                                $paths[$url][$method]['tags'][] = $class;
577
                            }
578
                            if(array_key_exists('payload', $endpoint)) {
579
                                $paths[$url][$method]['parameters'][] = [
580
                                    'in' => 'body',
581
                                    'name' => $class,
582
                                    'required' => true,
583
                                    'schema' => $classDefinition
584
                                ];
585
                            }
586
                        }
587
                    }
588
                }
589
            }
590
        }
591
        $formatted['definitions'] = $dtos;
592
        $formatted['paths'] = $paths;
593
        return $formatted;
594
    }
595
596
    /**
597
     * Method that extract the Dto class for the api documentation
598
     * @param string $dto
599
     * @param boolean $isArray
600
     *
601
     * @return string
602
     */
603
    protected function extractDtoName($dto, $isArray = false)
604
    {
605
        $dto = explode('\\', $dto);
606
        $modelDto = array_pop($dto) . "Dto";
607
        if ($isArray) {
608
            $modelDto .= "List";
609
        }
610
611
        return $modelDto;
612
    }
613
}
614