1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the API Platform project. |
5
|
|
|
* |
6
|
|
|
* (c) Kévin Dunglas <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace ApiPlatform\Core\Hydra\Serializer; |
13
|
|
|
|
14
|
|
|
use ApiPlatform\Core\Api\OperationMethodResolverInterface; |
15
|
|
|
use ApiPlatform\Core\Api\ResourceClassResolverInterface; |
16
|
|
|
use ApiPlatform\Core\Api\UrlGeneratorInterface; |
17
|
|
|
use ApiPlatform\Core\Documentation\Documentation; |
18
|
|
|
use ApiPlatform\Core\JsonLd\ContextBuilderInterface; |
19
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
20
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
21
|
|
|
use ApiPlatform\Core\Metadata\Property\PropertyMetadata; |
22
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
23
|
|
|
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; |
24
|
|
|
use Symfony\Component\PropertyInfo\Type; |
25
|
|
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Creates a machine readable Hydra API documentation. |
29
|
|
|
* |
30
|
|
|
* @author Kévin Dunglas <[email protected]> |
31
|
|
|
*/ |
32
|
|
|
final class DocumentationNormalizer implements NormalizerInterface |
33
|
|
|
{ |
34
|
|
|
const FORMAT = 'jsonld'; |
35
|
|
|
|
36
|
|
|
private $resourceMetadataFactory; |
37
|
|
|
private $propertyNameCollectionFactory; |
38
|
|
|
private $propertyMetadataFactory; |
39
|
|
|
private $resourceClassResolver; |
40
|
|
|
private $operationMethodResolver; |
41
|
|
|
private $urlGenerator; |
42
|
|
|
|
43
|
|
View Code Duplication |
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, UrlGeneratorInterface $urlGenerator) |
|
|
|
|
44
|
|
|
{ |
45
|
|
|
$this->resourceMetadataFactory = $resourceMetadataFactory; |
46
|
|
|
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
47
|
|
|
$this->propertyMetadataFactory = $propertyMetadataFactory; |
48
|
|
|
$this->resourceClassResolver = $resourceClassResolver; |
49
|
|
|
$this->operationMethodResolver = $operationMethodResolver; |
50
|
|
|
$this->urlGenerator = $urlGenerator; |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* {@inheritdoc} |
55
|
|
|
*/ |
56
|
|
|
public function normalize($object, $format = null, array $context = []) |
57
|
|
|
{ |
58
|
|
|
$classes = []; |
59
|
|
|
$entrypointProperties = []; |
60
|
|
|
|
61
|
|
|
foreach ($object->getResourceNameCollection() as $resourceClass) { |
62
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
63
|
|
|
|
64
|
|
|
$shortName = $resourceMetadata->getShortName(); |
65
|
|
|
$prefixedShortName = ($iri = $resourceMetadata->getIri()) ? $iri : '#'.$shortName; |
66
|
|
|
|
67
|
|
|
$collectionOperations = []; |
68
|
|
View Code Duplication |
if ($itemOperations = $resourceMetadata->getCollectionOperations()) { |
|
|
|
|
69
|
|
|
foreach ($itemOperations as $operationName => $collectionOperation) { |
70
|
|
|
$collectionOperations[] = $this->getHydraOperation($resourceClass, $resourceMetadata, $operationName, $collectionOperation, $prefixedShortName, true); |
71
|
|
|
} |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
if (!empty($collectionOperations)) { |
75
|
|
|
$entrypointProperties[] = [ |
76
|
|
|
'@type' => 'hydra:SupportedProperty', |
77
|
|
|
'hydra:property' => [ |
78
|
|
|
'@id' => sprintf('#Entrypoint/%s', lcfirst($shortName)), |
79
|
|
|
'@type' => 'hydra:Link', |
80
|
|
|
'domain' => '#Entrypoint', |
81
|
|
|
'rdfs:label' => sprintf('The collection of %s resources', $shortName), |
82
|
|
|
'range' => 'hydra:PagedCollection', |
83
|
|
|
'hydra:supportedOperation' => $collectionOperations, |
84
|
|
|
], |
85
|
|
|
'hydra:title' => sprintf('The collection of %s resources', $shortName), |
86
|
|
|
'hydra:readable' => true, |
87
|
|
|
'hydra:writable' => false, |
88
|
|
|
]; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
$class = [ |
92
|
|
|
'@id' => $prefixedShortName, |
93
|
|
|
'@type' => 'hydra:Class', |
94
|
|
|
'rdfs:label' => $shortName, |
95
|
|
|
'hydra:title' => $shortName, |
96
|
|
|
]; |
97
|
|
|
|
98
|
|
|
if ($description = $resourceMetadata->getDescription()) { |
99
|
|
|
$class['hydra:description'] = $description; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
$attributes = $resourceMetadata->getAttributes(); |
103
|
|
|
$context = []; |
104
|
|
|
$properties = []; |
105
|
|
|
|
106
|
|
View Code Duplication |
if (isset($attributes['normalization_context']['groups'])) { |
|
|
|
|
107
|
|
|
$context['serializer_groups'] = $attributes['normalization_context']['groups']; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
View Code Duplication |
if (isset($attributes['denormalization_context']['groups'])) { |
|
|
|
|
111
|
|
|
$context['serializer_groups'] = isset($context['serializer_groups']) ? array_merge($context['serializer_groups'], $attributes['denormalization_context']['groups']) : $context['serializer_groups']; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
foreach ($this->propertyNameCollectionFactory->create($resourceClass, $context) as $propertyName) { |
115
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); |
116
|
|
|
if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) { |
117
|
|
|
continue; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
$class['hydra:supportedProperty'] = $properties; |
124
|
|
|
|
125
|
|
|
$itemOperations = []; |
126
|
|
|
|
127
|
|
View Code Duplication |
if ($operations = $resourceMetadata->getItemOperations()) { |
|
|
|
|
128
|
|
|
foreach ($operations as $operationName => $itemOperation) { |
129
|
|
|
$itemOperations[] = $this->getHydraOperation($resourceClass, $resourceMetadata, $operationName, $itemOperation, $prefixedShortName, false); |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
$class['hydra:supportedOperation'] = $itemOperations; |
134
|
|
|
$classes[] = $class; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$classes = $this->getClasses($entrypointProperties, $classes); |
138
|
|
|
|
139
|
|
|
return $this->computeDoc($object, $classes); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Gets and populates if applicable a Hydra operation. |
144
|
|
|
*/ |
145
|
|
|
private function getHydraOperation(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, string $prefixedShortName, bool $collection) : array |
146
|
|
|
{ |
147
|
|
|
if ($collection) { |
148
|
|
|
$method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName); |
149
|
|
|
} else { |
150
|
|
|
$method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
$hydraOperation = $operation['hydra_context'] ?? []; |
154
|
|
|
$shortName = $resourceMetadata->getShortName(); |
155
|
|
|
|
156
|
|
|
switch ($method) { |
157
|
|
|
case 'GET': |
|
|
|
|
158
|
|
|
if ($collection) { |
159
|
|
|
if (!isset($hydraOperation['hydra:title'])) { |
160
|
|
|
$hydraOperation['hydra:title'] = sprintf('Retrieves the collection of %s resources.', $shortName); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
if (!isset($hydraOperation['returns'])) { |
164
|
|
|
$hydraOperation['returns'] = 'hydra:PagedCollection'; |
165
|
|
|
} |
166
|
|
|
} else { |
167
|
|
|
if (!isset($hydraOperation['hydra:title'])) { |
168
|
|
|
$hydraOperation['hydra:title'] = sprintf('Retrieves %s resource.', $shortName); |
169
|
|
|
} |
170
|
|
|
} |
171
|
|
|
break; |
172
|
|
|
|
173
|
|
View Code Duplication |
case 'POST': |
|
|
|
|
174
|
|
|
if (!isset($hydraOperation['@type'])) { |
175
|
|
|
$hydraOperation['@type'] = 'hydra:CreateResourceOperation'; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
if (!isset($hydraOperation['hydra:title'])) { |
179
|
|
|
$hydraOperation['hydra:title'] = sprintf('Creates a %s resource.', $shortName); |
180
|
|
|
} |
181
|
|
|
break; |
182
|
|
|
|
183
|
|
View Code Duplication |
case 'PUT': |
|
|
|
|
184
|
|
|
if (!isset($hydraOperation['@type'])) { |
185
|
|
|
$hydraOperation['@type'] = 'hydra:ReplaceResourceOperation'; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
if (!isset($hydraOperation['hydra:title'])) { |
189
|
|
|
$hydraOperation['hydra:title'] = sprintf('Replaces the %s resource.', $shortName); |
190
|
|
|
} |
191
|
|
|
break; |
192
|
|
|
|
193
|
|
View Code Duplication |
case 'DELETE': |
|
|
|
|
194
|
|
|
if (!isset($hydraOperation['hydra:title'])) { |
195
|
|
|
$hydraOperation['hydra:title'] = sprintf('Deletes the %s resource.', $shortName); |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
if (!isset($hydraOperation['returns'])) { |
199
|
|
|
$hydraOperation['returns'] = 'owl:Nothing'; |
200
|
|
|
} |
201
|
|
|
break; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
View Code Duplication |
if (!isset($hydraOperation['returns']) && |
|
|
|
|
205
|
|
|
( |
206
|
|
|
('GET' === $method && !$collection) || |
207
|
|
|
'POST' === $method || |
208
|
|
|
'PUT' === $method |
209
|
|
|
) |
210
|
|
|
) { |
211
|
|
|
$hydraOperation['returns'] = $prefixedShortName; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
View Code Duplication |
if (!isset($hydraOperation['expects']) && |
|
|
|
|
215
|
|
|
('POST' === $method || 'PUT' === $method)) { |
216
|
|
|
$hydraOperation['expects'] = $prefixedShortName; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
if (!isset($hydraOperation['@type'])) { |
220
|
|
|
$hydraOperation['@type'] = 'hydra:Operation'; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
if (!isset($hydraOperation['hydra:method'])) { |
224
|
|
|
$hydraOperation['hydra:method'] = $method; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) { |
228
|
|
|
$hydraOperation['rdfs:label'] = $hydraOperation['hydra:title']; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
ksort($hydraOperation); |
232
|
|
|
|
233
|
|
|
return $hydraOperation; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* Gets the range of the property. |
238
|
|
|
* |
239
|
|
|
* @param PropertyMetadata $propertyMetadata |
240
|
|
|
* |
241
|
|
|
* @return string|null |
242
|
|
|
*/ |
243
|
|
|
private function getRange(PropertyMetadata $propertyMetadata) |
244
|
|
|
{ |
245
|
|
|
$type = $propertyMetadata->getType(); |
246
|
|
|
if (!$type) { |
247
|
|
|
return; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
if ($type->isCollection() && $collectionType = $type->getCollectionValueType()) { |
251
|
|
|
$type = $collectionType; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
switch ($type->getBuiltinType()) { |
255
|
|
|
case Type::BUILTIN_TYPE_STRING: |
256
|
|
|
return 'xmls:string'; |
257
|
|
|
|
258
|
|
|
case Type::BUILTIN_TYPE_INT: |
259
|
|
|
return 'xmls:integer'; |
260
|
|
|
|
261
|
|
|
case Type::BUILTIN_TYPE_FLOAT: |
262
|
|
|
return 'xmls:number'; |
263
|
|
|
|
264
|
|
|
case Type::BUILTIN_TYPE_BOOL: |
265
|
|
|
return 'xmls:boolean'; |
266
|
|
|
|
267
|
|
|
case Type::BUILTIN_TYPE_OBJECT: |
268
|
|
|
$className = $type->getClassName(); |
269
|
|
|
|
270
|
|
|
if (null !== $className) { |
271
|
|
|
$reflection = new \ReflectionClass($className); |
272
|
|
|
if ($reflection->implementsInterface(\DateTimeInterface::class)) { |
273
|
|
|
return 'xmls:dateTime'; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
$className = $type->getClassName(); |
277
|
|
|
if ($this->resourceClassResolver->isResourceClass($className)) { |
278
|
|
|
return sprintf('#%s', $this->resourceMetadataFactory->create($className)->getShortName()); |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
break; |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/* |
286
|
|
|
* Builds the classes array. |
287
|
|
|
*/ |
288
|
|
|
private function getClasses(array $entrypointProperties, array $classes) : array |
289
|
|
|
{ |
290
|
|
|
$classes[] = [ |
291
|
|
|
'@id' => '#Entrypoint', |
292
|
|
|
'@type' => 'hydra:Class', |
293
|
|
|
'hydra:title' => 'The API entrypoint', |
294
|
|
|
'hydra:supportedProperty' => $entrypointProperties, |
295
|
|
|
'hydra:supportedOperation' => [ |
296
|
|
|
'@type' => 'hydra:Operation', |
297
|
|
|
'hydra:method' => 'GET', |
298
|
|
|
'rdfs:label' => 'The API entrypoint.', |
299
|
|
|
'returns' => '#EntryPoint', |
300
|
|
|
], |
301
|
|
|
]; |
302
|
|
|
|
303
|
|
|
// Constraint violation |
304
|
|
|
$classes[] = [ |
305
|
|
|
'@id' => '#ConstraintViolation', |
306
|
|
|
'@type' => 'hydra:Class', |
307
|
|
|
'hydra:title' => 'A constraint violation', |
308
|
|
|
'hydra:supportedProperty' => [ |
309
|
|
|
[ |
310
|
|
|
'@type' => 'hydra:SupportedProperty', |
311
|
|
|
'hydra:property' => [ |
312
|
|
|
'@id' => '#ConstraintViolation/propertyPath', |
313
|
|
|
'@type' => 'rdf:Property', |
314
|
|
|
'rdfs:label' => 'propertyPath', |
315
|
|
|
'domain' => '#ConstraintViolation', |
316
|
|
|
'range' => 'xmls:string', |
317
|
|
|
], |
318
|
|
|
'hydra:title' => 'propertyPath', |
319
|
|
|
'hydra:description' => 'The property path of the violation', |
320
|
|
|
'hydra:readable' => true, |
321
|
|
|
'hydra:writable' => false, |
322
|
|
|
], |
323
|
|
|
[ |
324
|
|
|
'@type' => 'hydra:SupportedProperty', |
325
|
|
|
'hydra:property' => [ |
326
|
|
|
'@id' => '#ConstraintViolation/message', |
327
|
|
|
'@type' => 'rdf:Property', |
328
|
|
|
'rdfs:label' => 'message', |
329
|
|
|
'domain' => '#ConstraintViolation', |
330
|
|
|
'range' => 'xmls:string', |
331
|
|
|
], |
332
|
|
|
'hydra:title' => 'message', |
333
|
|
|
'hydra:description' => 'The message associated with the violation', |
334
|
|
|
'hydra:readable' => true, |
335
|
|
|
'hydra:writable' => false, |
336
|
|
|
], |
337
|
|
|
], |
338
|
|
|
]; |
339
|
|
|
|
340
|
|
|
// Constraint violation list |
341
|
|
|
$classes[] = [ |
342
|
|
|
'@id' => '#ConstraintViolationList', |
343
|
|
|
'@type' => 'hydra:Class', |
344
|
|
|
'subClassOf' => 'hydra:Error', |
345
|
|
|
'hydra:title' => 'A constraint violation list', |
346
|
|
|
'hydra:supportedProperty' => [ |
347
|
|
|
[ |
348
|
|
|
'@type' => 'hydra:SupportedProperty', |
349
|
|
|
'hydra:property' => [ |
350
|
|
|
'@id' => '#ConstraintViolationList/violation', |
351
|
|
|
'@type' => 'rdf:Property', |
352
|
|
|
'rdfs:label' => 'violation', |
353
|
|
|
'domain' => '#ConstraintViolationList', |
354
|
|
|
'range' => '#ConstraintViolation', |
355
|
|
|
], |
356
|
|
|
'hydra:title' => 'violation', |
357
|
|
|
'hydra:description' => 'The violations', |
358
|
|
|
'hydra:readable' => true, |
359
|
|
|
'hydra:writable' => false, |
360
|
|
|
], |
361
|
|
|
], |
362
|
|
|
]; |
363
|
|
|
|
364
|
|
|
return $classes; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
private function getProperty(PropertyMetadata $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName): array |
368
|
|
|
{ |
369
|
|
|
$type = $propertyMetadata->isReadableLink() ? 'rdf:Property' : 'Hydra:Link'; |
370
|
|
|
$property = [ |
371
|
|
|
'@type' => 'hydra:SupportedProperty', |
372
|
|
|
'hydra:property' => [ |
373
|
|
|
'@id' => ($iri = $propertyMetadata->getIri()) ? $iri : sprintf('#%s/%s', $shortName, $propertyName), |
374
|
|
|
'@type' => $type, |
375
|
|
|
'rdfs:label' => $propertyName, |
376
|
|
|
'domain' => $prefixedShortName, |
377
|
|
|
], |
378
|
|
|
'hydra:title' => $propertyName, |
379
|
|
|
'hydra:required' => $propertyMetadata->isRequired(), |
380
|
|
|
'hydra:readable' => $propertyMetadata->isReadable(), |
381
|
|
|
'hydra:writable' => $propertyMetadata->isWritable(), |
382
|
|
|
]; |
383
|
|
|
|
384
|
|
|
if ($range = $this->getRange($propertyMetadata)) { |
385
|
|
|
$property['hydra:property']['range'] = $range; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
if ($description = $propertyMetadata->getDescription()) { |
389
|
|
|
$property['hydra:description'] = $description; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
return $property; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
private function computeDoc(Documentation $object, array $classes): array |
396
|
|
|
{ |
397
|
|
|
$doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT])]; |
398
|
|
|
|
399
|
|
|
if ('' !== $object->getTitle()) { |
400
|
|
|
$doc['hydra:title'] = $object->getTitle(); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
if ('' !== $object->getDescription()) { |
404
|
|
|
$doc['hydra:description'] = $object->getDescription(); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
$doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint'); |
408
|
|
|
$doc['hydra:supportedClass'] = $classes; |
409
|
|
|
|
410
|
|
|
return $doc; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
/** |
414
|
|
|
* Builds the JSON-LD context for the API documentation. |
415
|
|
|
* |
416
|
|
|
* @return array |
417
|
|
|
*/ |
418
|
|
|
private function getContext() : array |
419
|
|
|
{ |
420
|
|
|
return [ |
421
|
|
|
'@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#', |
422
|
|
|
'hydra' => ContextBuilderInterface::HYDRA_NS, |
423
|
|
|
'rdf' => ContextBuilderInterface::RDF_NS, |
424
|
|
|
'rdfs' => ContextBuilderInterface::RDFS_NS, |
425
|
|
|
'xmls' => ContextBuilderInterface::XML_NS, |
426
|
|
|
'owl' => ContextBuilderInterface::OWL_NS, |
427
|
|
|
'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'], |
428
|
|
|
'range' => ['@id' => 'rdfs:range', '@type' => '@id'], |
429
|
|
|
'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'], |
430
|
|
|
'expects' => ['@id' => 'hydra:expects', '@type' => '@id'], |
431
|
|
|
'returns' => ['@id' => 'hydra:returns', '@type' => '@id'], |
432
|
|
|
]; |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* {@inheritdoc} |
437
|
|
|
*/ |
438
|
|
|
public function supportsNormalization($data, $format = null, array $context = []) |
439
|
|
|
{ |
440
|
|
|
return self::FORMAT === $format && $data instanceof Documentation; |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
|
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.