Completed
Push — feature/EVO-7278-security-and-... ( 0e0414 )
by
unknown
13:20
created

RestController::patchAction()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 49
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 49
ccs 0
cts 32
cp 0
rs 8.5906
c 0
b 0
f 0
cc 6
eloc 27
nc 10
nop 2
crap 42
1
<?php
2
/**
3
 * basic rest controller
4
 */
5
6
namespace Graviton\RestBundle\Controller;
7
8
use Graviton\ExceptionBundle\Exception\DeserializationException;
9
use Graviton\ExceptionBundle\Exception\InvalidJsonPatchException;
10
use Graviton\ExceptionBundle\Exception\MalformedInputException;
11
use Graviton\ExceptionBundle\Exception\NotFoundException;
12
use Graviton\ExceptionBundle\Exception\SerializationException;
13
use Graviton\JsonSchemaBundle\Exception\ValidationException;
14
use Graviton\RestBundle\Model\DocumentModel;
15
use Graviton\SchemaBundle\SchemaUtils;
16
use Graviton\RestBundle\Service\RestUtilsInterface;
17
use Graviton\SecurityBundle\Entities\SecurityUser;
18
use Graviton\SecurityBundle\Service\SecurityUtils;
19
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
20
use Symfony\Component\Security\Core\User\UserInterface;
21
use Symfony\Component\DependencyInjection\ContainerInterface;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpFoundation\Response;
24
use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
25
use Symfony\Component\Routing\Exception\RouteNotFoundException;
26
use Symfony\Bundle\FrameworkBundle\Routing\Router;
27
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
28
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
29
use Rs\Json\Patch;
30
use Rs\Json\Patch\InvalidPatchDocumentJsonException;
31
use Rs\Json\Patch\InvalidTargetDocumentJsonException;
32
use Rs\Json\Patch\InvalidOperationException;
33
use Rs\Json\Patch\FailedTestException;
34
use Graviton\RestBundle\Service\JsonPatchValidator;
35
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
36
37
/**
38
 * This is a basic rest controller. It should fit the most needs but if you need to add some
39
 * extra functionality you can extend it and overwrite single/all actions.
40
 * You can also extend the model class to add some extra logic before save
41
 *
42
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
43
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
44
 * @link     http://swisscom.ch
45
 */
46
class RestController
47
{
48
    /**
49
     * @var DocumentModel
50
     */
51
    private $model;
52
53
    /**
54
     * @var ContainerInterface service_container
55
     */
56
    private $container;
57
58
    /**
59
     * @var Response
60
     */
61
    private $response;
62
63
    /**
64
     * @var RestUtilsInterface
65
     */
66
    private $restUtils;
67
68
    /**
69
     * @var SchemaUtils
70
     */
71
    private $schemaUtils;
72
73
    /**
74
     * @var Router
75
     */
76
    private $router;
77
78
    /**
79
     * @var EngineInterface
80
     */
81
    private $templating;
82
83
    /**
84
     * @var JsonPatchValidator
85
     */
86
    private $jsonPatchValidator;
87
88
    /**
89
     * @var SecurityUtils
90
     */
91
    protected $securityUtils;
92
93
    /**
94
     * @param Response           $response    Response
95
     * @param RestUtilsInterface $restUtils   Rest utils
96
     * @param Router             $router      Router
97
     * @param EngineInterface    $templating  Templating
98
     * @param ContainerInterface $container   Container
99
     * @param SchemaUtils        $schemaUtils Schema utils
100
     */
101
    public function __construct(
102
        Response $response,
103
        RestUtilsInterface $restUtils,
104
        Router $router,
105
        EngineInterface $templating,
106
        ContainerInterface $container,
107
        SchemaUtils $schemaUtils
108
    ) {
109
        $this->response = $response;
110
        $this->restUtils = $restUtils;
111
        $this->router = $router;
112
        $this->templating = $templating;
113
        $this->container = $container;
114
        $this->schemaUtils = $schemaUtils;
115
    }
116
117
    /**
118
     * Setter for the SecurityUtils
119
     *
120
     * @param SecurityUtils $securityUtils The securityUtils service
121
     * @return void
122
     */
123
    public function setSecurityUtils(SecurityUtils $securityUtils)
124
    {
125
        $this->securityUtils = $securityUtils;
126
    }
127
128
    /**
129
     * @param JsonPatchValidator $jsonPatchValidator Service for validation json patch
130
     * @return void
131
     */
132
    public function setJsonPatchValidator(JsonPatchValidator $jsonPatchValidator)
133
    {
134
        $this->jsonPatchValidator = $jsonPatchValidator;
135
    }
136
137
    /**
138
     * Get the container object
139
     *
140
     * @return \Symfony\Component\DependencyInjection\ContainerInterface
141
     *
142
     * @obsolete
143
     */
144
    public function getContainer()
145
    {
146
        return $this->container;
147
    }
148
149
    /**
150
     * Returns a single record
151
     *
152
     * @param Request $request Current http request
153
     * @param string  $id      ID of record
154
     *
155
     * @return \Symfony\Component\HttpFoundation\Response $response Response with result or error
156
     */
157 View Code Duplication
    public function getAction(Request $request, $id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
158
    {
159
        $response = $this->getResponse()
160
            ->setStatusCode(Response::HTTP_OK)
161
            ->setContent($this->serialize($this->findRecord($id, $request)));
162
163
        return $response;
164
    }
165
166
    /**
167
     * Get the response object
168
     *
169
     * @return \Symfony\Component\HttpFoundation\Response $response Response object
170
     */
171
    public function getResponse()
172
    {
173
        return $this->response;
174
    }
175
176
    /**
177
     * Get a single record from database or throw an exception if it doesn't exist
178
     *
179
     * @param mixed   $id      Record id
180
     * @param Request $request request
0 ignored issues
show
Documentation introduced by
Should the type for parameter $request not be null|Request?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
181
     *
182
     * @throws \Graviton\ExceptionBundle\Exception\NotFoundException
183
     *
184
     * @return object $record Document object
185
     */
186
    protected function findRecord($id, Request $request = null)
187
    {
188
        $response = $this->getResponse();
189
190
        if (!($this->getModel()->recordExists($id))) {
191
            $e = new NotFoundException("Entry with id " . $id . " not found!");
192
            $e->setResponse($response);
193
            throw $e;
194
        }
195
196
        return $this->getModel()->find($id, $request);
197
    }
198
199
    /**
200
     * Return the model
201
     *
202
     * @throws \Exception in case no model was defined.
203
     *
204
     * @return DocumentModel $model Model
205
     */
206
    public function getModel()
207
    {
208
        if (!$this->model) {
209
            throw new \Exception('No model is set for this controller');
210
        }
211
212
        return $this->model;
213
    }
214
215
    /**
216
     * Set the model class
217
     *
218
     * @param DocumentModel $model Model class
219
     *
220
     * @return self
221
     */
222
    public function setModel(DocumentModel $model)
223
    {
224
        $this->model = $model;
225
226
        return $this;
227
    }
228
229
    /**
230
     * Serialize the given record and throw an exception if something went wrong
231
     *
232
     * @param object|object[] $result Record(s)
233
     *
234
     * @throws \Graviton\ExceptionBundle\Exception\SerializationException
235
     *
236
     * @return string $content Json content
237
     */
238
    protected function serialize($result)
239
    {
240
        $response = $this->getResponse();
241
242
        try {
243
            // array is serialized as an object {"0":{...},"1":{...},...} when data contains an empty objects
244
            // we serialize each item because we can assume this bug affects only root array element
245
            if (is_array($result) && array_keys($result) === range(0, count($result) - 1)) {
246
                $result = array_map(
247
                    function ($item) {
248
                        return $this->getRestUtils()->serializeContent($item);
249
                    },
250
                    $result
251
                );
252
253
                return '['.implode(',', array_filter($result)).']';
254
            }
255
256
            return $this->getRestUtils()->serializeContent($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by parameter $result on line 238 can also be of type array; however, Graviton\RestBundle\Serv...ils::serializeContent() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
257
        } catch (\Exception $e) {
258
            $exception = new SerializationException($e);
259
            $exception->setResponse($response);
260
            throw $exception;
261
        }
262
    }
263
264
    /**
265
     * Get RestUtils service
266
     *
267
     * @return \Graviton\RestBundle\Service\RestUtils
268
     */
269
    public function getRestUtils()
270
    {
271
        return $this->restUtils;
272
    }
273
274
    /**
275
     * Returns all records
276
     *
277
     * @param Request $request Current http request
278
     *
279
     * @return \Symfony\Component\HttpFoundation\Response $response Response with result or error
280
     */
281 View Code Duplication
    public function allAction(Request $request)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
282
    {
283
        $model = $this->getModel();
284
285
        $response = $this->getResponse()
286
            ->setStatusCode(Response::HTTP_OK)
287
            ->setContent($this->serialize($model->findAll($request)));
288
289
        return $response;
290
    }
291
292
    /**
293
     * Writes a new Entry to the database
294
     *
295
     * @param Request $request Current http request
296
     *
297
     * @return \Symfony\Component\HttpFoundation\Response $response Result of action with data (if successful)
298
     */
299
    public function postAction(Request $request)
300
    {
301
        // Get the response object from container
302
        $response = $this->getResponse();
303
        $model = $this->getModel();
304
305
        $this->restUtils->checkJsonRequest($request, $response, $this->getModel());
306
307
        $record = $this->validateRequest($request->getContent(), $model);
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Graviton\RestBundle\Cont...ller::validateRequest() does only seem to accept object|string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
308
309
        // Insert the new record
310
        $record = $this->getModel()->insertRecord($record);
311
312
        // store id of new record so we dont need to reparse body later when needed
313
        $request->attributes->set('id', $record->getId());
314
315
        // Set status code
316
        $response->setStatusCode(Response::HTTP_CREATED);
317
318
        $response->headers->set(
319
            'Location',
320
            $this->getRouter()->generate($this->getRouteName($request), array('id' => $record->getId()))
321
        );
322
323
        return $response;
324
    }
325
326
    /**
327
     * Validates the current request on schema violations. If there are errors,
328
     * the exception is thrown. If not, the deserialized record is returned.
329
     *
330
     * @param object|string $content \stdClass of the request content
331
     * @param DocumentModel $model   the model to check the schema for
332
     *
333
     * @return \Graviton\JsonSchemaBundle\Exception\ValidationExceptionError[]
0 ignored issues
show
Documentation introduced by
Should the return type not be object|array|integer|double|string|boolean? 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...
334
     * @throws \Exception
335
     */
336
    protected function validateRequest($content, DocumentModel $model)
337
    {
338
        $errors = $this->restUtils->validateContent($content, $model);
339
        if (!empty($errors)) {
340
            throw new ValidationException($errors);
341
        }
342
        return $this->deserialize($content, $model->getEntityClass());
0 ignored issues
show
Bug introduced by
It seems like $content defined by parameter $content on line 336 can also be of type object; however, Graviton\RestBundle\Cont...ntroller::deserialize() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
343
    }
344
345
    /**
346
     * Deserialize the given content throw an exception if something went wrong
347
     *
348
     * @param string $content       Request content
349
     * @param string $documentClass Document class
350
     *
351
     * @throws DeserializationException
352
     *
353
     * @return object $record Document
0 ignored issues
show
Documentation introduced by
Should the return type not be object|array|integer|double|string|boolean? 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...
354
     */
355
    protected function deserialize($content, $documentClass)
356
    {
357
        $response = $this->getResponse();
358
359
        try {
360
            $record = $this->getRestUtils()->deserializeContent(
361
                $content,
362
                $documentClass
363
            );
364
        } catch (\Exception $e) {
365
            // pass the previous exception in this case to get the error message in the handler
366
            // http://php.net/manual/de/exception.getprevious.php
367
            $exception = new DeserializationException("Deserialization failed", $e);
368
369
            // at the moment, the response has to be set on the exception object.
370
            // try to refactor this and return the graviton.rest.response if none is set...
371
            $exception->setResponse($response);
372
            throw $exception;
373
        }
374
375
        return $record;
376
    }
377
378
    /**
379
     * Get the router from the dic
380
     *
381
     * @return Router
382
     */
383
    public function getRouter()
384
    {
385
        return $this->router;
386
    }
387
388
    /**
389
     * Update a record
390
     *
391
     * @param Number  $id      ID of record
392
     * @param Request $request Current http request
393
     *
394
     * @throws MalformedInputException
395
     *
396
     * @return Response $response Result of action with data (if successful)
397
     */
398
    public function putAction($id, Request $request)
399
    {
400
        $response = $this->getResponse();
401
        $model = $this->getModel();
402
403
        $this->restUtils->checkJsonRequest($request, $response, $this->getModel());
404
405
        $record = $this->validateRequest($request->getContent(), $model);
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Graviton\RestBundle\Cont...ller::validateRequest() does only seem to accept object|string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
406
407
        // handle missing 'id' field in input to a PUT operation
408
        // if it is settable on the document, let's set it and move on.. if not, inform the user..
409
        if ($record->getId() != $id) {
410
            // try to set it..
411
            if (is_callable(array($record, 'setId'))) {
412
                $record->setId($id);
413
            } else {
414
                throw new MalformedInputException('No ID was supplied in the request payload.');
415
            }
416
        }
417
418
        // And update the record, if everything is ok
419
        if (!$this->getModel()->recordExists($id)) {
420
            $this->getModel()->insertRecord($record, false);
421
        } else {
422
            $this->getModel()->updateRecord($id, $record, false);
423
        }
424
425
        // Set status code
426
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
427
428
        // store id of new record so we dont need to reparse body later when needed
429
        $request->attributes->set('id', $record->getId());
430
431
        return $response;
432
    }
433
434
    /**
435
     * Patch a record
436
     *
437
     * @param Number  $id      ID of record
438
     * @param Request $request Current http request
439
     *
440
     * @throws MalformedInputException
441
     *
442
     * @return Response $response Result of action with data (if successful)
443
     */
444
    public function patchAction($id, Request $request)
445
    {
446
        $response = $this->getResponse();
447
448
        // Check JSON Patch request
449
        $this->restUtils->checkJsonRequest($request, $response, $this->getModel());
450
        $this->restUtils->checkJsonPatchRequest(json_decode($request->getContent(), 1));
451
452
        // Find record && apply $ref converter
453
        $record = $this->findRecord($id);
454
        $jsonDocument = $this->serialize($record);
455
456
        // Check/validate JSON Patch
457
        if (!$this->jsonPatchValidator->validate($jsonDocument, $request->getContent())) {
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Graviton\RestBundle\Serv...chValidator::validate() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
458
            throw new InvalidJsonPatchException($this->jsonPatchValidator->getException()->getMessage());
459
        }
460
461
        try {
462
            // Apply JSON patches
463
            $patch = new Patch($jsonDocument, $request->getContent());
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Rs\Json\Patch::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
464
            $patchedDocument = $patch->apply();
465
        } catch (InvalidPatchDocumentJsonException $e) {
466
            throw new InvalidJsonPatchException($e->getMessage());
467
        } catch (InvalidTargetDocumentJsonException $e) {
468
            throw new InvalidJsonPatchException($e->getMessage());
469
        } catch (InvalidOperationException $e) {
470
            throw new InvalidJsonPatchException($e->getMessage());
471
        } catch (FailedTestException $e) {
472
            throw new InvalidJsonPatchException($e->getMessage());
473
        }
474
475
        // Validate result object
476
        $model = $this->getModel();
477
        $record = $this->validateRequest($patchedDocument, $model);
478
479
        // Update object
480
        $this->getModel()->updateRecord($id, $record);
481
482
        // Set status code
483
        $response->setStatusCode(Response::HTTP_OK);
484
485
        // Set Content-Location header
486
        $response->headers->set(
487
            'Content-Location',
488
            $this->getRouter()->generate($this->getRouteName($request), array('id' => $record->getId()))
489
        );
490
491
        return $response;
492
    }
493
494
    /**
495
     * Deletes a record
496
     *
497
     * @param Number $id ID of record
498
     *
499
     * @return Response $response Result of the action
500
     */
501
    public function deleteAction($id)
502
    {
503
        $response = $this->getResponse();
504
505
        // does this record exist?
506
        $this->findRecord($id);
507
508
        $this->getModel()->deleteRecord($id);
509
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
510
511
        return $response;
512
    }
513
514
    /**
515
     * Return OPTIONS results.
516
     *
517
     * @param Request $request Current http request
518
     *
519
     * @throws SerializationException
520
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
521
     */
522
    public function optionsAction(Request $request)
523
    {
524
        list($app, $module, , $modelName) = explode('.', $request->attributes->get('_route'));
525
526
        $response = $this->response;
527
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
528
529
        // enabled methods for CorsListener
530
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
531
        try {
532
            $router = $this->getRouter();
533
            // if post route is available we assume everything is readable
534
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
535
        } catch (RouteNotFoundException $exception) {
536
            // only allow read methods
537
            $corsMethods = 'GET, OPTIONS';
538
        }
539
        $request->attributes->set('corsMethods', $corsMethods);
540
541
        return $response;
542
    }
543
544
545
    /**
546
     * Return schema GET results.
547
     *
548
     * @param Request $request Current http request
549
     * @param string  $id      ID of record
0 ignored issues
show
Documentation introduced by
Should the type for parameter $id not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
550
     *
551
     * @throws SerializationException
552
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
553
     */
554
    public function schemaAction(Request $request, $id = null)
555
    {
556
        $request->attributes->set('schemaRequest', true);
557
558
        list($app, $module, , $modelName, $schemaType) = explode('.', $request->attributes->get('_route'));
559
560
        $response = $this->response;
561
        $response->setStatusCode(Response::HTTP_OK);
562
        $response->setPublic();
563
564
        if (!$id && $schemaType != 'canonicalIdSchema') {
565
            $schema = $this->schemaUtils->getCollectionSchema($modelName, $this->getModel());
566
        } else {
567
            $schema = $this->schemaUtils->getModelSchema($modelName, $this->getModel());
568
        }
569
570
        // enabled methods for CorsListener
571
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
572
        try {
573
            $router = $this->getRouter();
574
            // if post route is available we assume everything is readable
575
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
576
        } catch (RouteNotFoundException $exception) {
577
            // only allow read methods
578
            $corsMethods = 'GET, OPTIONS';
579
        }
580
        $request->attributes->set('corsMethods', $corsMethods);
581
582
583
        return $this->render(
584
            'GravitonRestBundle:Main:index.json.twig',
585
            ['response' => $this->serialize($schema)],
586
            $response
587
        );
588
    }
589
590
    /**
591
     * Renders a view.
592
     *
593
     * @param string   $view       The view name
594
     * @param array    $parameters An array of parameters to pass to the view
595
     * @param Response $response   A response instance
0 ignored issues
show
Documentation introduced by
Should the type for parameter $response not be null|Response?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
596
     *
597
     * @return Response A Response instance
598
     */
599
    public function render($view, array $parameters = array(), Response $response = null)
600
    {
601
        return $this->templating->renderResponse($view, $parameters, $response);
602
    }
603
604
    /**
605
     * @param Request $request request
606
     * @return string
607
     */
608
    private function getRouteName(Request $request)
609
    {
610
        $routeName = $request->get('_route');
611
        $routeParts = explode('.', $routeName);
612
        $routeType = end($routeParts);
613
614
        if ($routeType == 'post') {
615
            $routeName = substr($routeName, 0, -4) . 'get';
616
        }
617
618
        return $routeName;
619
    }
620
621
    /**
622
     * Security needs to be enabled to get Object.
623
     *
624
     * @return SecurityUser
625
     * @throws UsernameNotFoundException
626
     */
627
    public function getSecurityUser()
628
    {
629
        return $this->securityUtils->getSecurityUser();
630
    }
631
}
632