1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Sync; |
4: | |
5: | use Salient\Container\RequiresContainer; |
6: | use Salient\Contract\Container\ContainerInterface; |
7: | use Salient\Contract\Core\Entity\Readable; |
8: | use Salient\Contract\Core\Entity\Writable; |
9: | use Salient\Contract\Core\Provider\ProviderContextInterface; |
10: | use Salient\Contract\Core\Provider\ProviderInterface; |
11: | use Salient\Contract\Core\DateFormatterInterface; |
12: | use Salient\Contract\Core\Flushable; |
13: | use Salient\Contract\Core\HasDescription; |
14: | use Salient\Contract\Core\ListConformity; |
15: | use Salient\Contract\Core\NormaliserFlag; |
16: | use Salient\Contract\Core\TextComparisonAlgorithm as Algorithm; |
17: | use Salient\Contract\Core\TextComparisonFlag as Flag; |
18: | use Salient\Contract\Iterator\FluentIteratorInterface; |
19: | use Salient\Contract\Sync\DeferredEntityInterface; |
20: | use Salient\Contract\Sync\DeferredRelationshipInterface; |
21: | use Salient\Contract\Sync\EntityState; |
22: | use Salient\Contract\Sync\LinkType; |
23: | use Salient\Contract\Sync\SyncContextInterface; |
24: | use Salient\Contract\Sync\SyncEntityInterface; |
25: | use Salient\Contract\Sync\SyncEntityProviderInterface; |
26: | use Salient\Contract\Sync\SyncProviderInterface; |
27: | use Salient\Contract\Sync\SyncSerializeRulesInterface; |
28: | use Salient\Contract\Sync\SyncStoreInterface; |
29: | use Salient\Core\Concern\ConstructibleTrait; |
30: | use Salient\Core\Concern\ExtensibleTrait; |
31: | use Salient\Core\Concern\HasNormaliser; |
32: | use Salient\Core\Concern\HasReadableProperties; |
33: | use Salient\Core\Concern\HasWritableProperties; |
34: | use Salient\Core\Concern\ProvidableTrait; |
35: | use Salient\Core\Facade\Sync; |
36: | use Salient\Core\AbstractEntity; |
37: | use Salient\Core\DateFormatter; |
38: | use Salient\Iterator\IterableIterator; |
39: | use Salient\Sync\Exception\SyncEntityNotFoundException; |
40: | use Salient\Sync\Support\SyncIntrospector; |
41: | use Salient\Utility\Arr; |
42: | use Salient\Utility\Get; |
43: | use Salient\Utility\Inflect; |
44: | use Salient\Utility\Regex; |
45: | use Salient\Utility\Str; |
46: | use DateTimeInterface; |
47: | use Generator; |
48: | use LogicException; |
49: | use ReflectionClass; |
50: | use UnexpectedValueException; |
51: | |
52: | |
53: | |
54: | |
55: | |
56: | |
57: | |
58: | |
59: | |
60: | |
61: | |
62: | |
63: | |
64: | |
65: | |
66: | |
67: | |
68: | |
69: | |
70: | |
71: | |
72: | |
73: | |
74: | |
75: | abstract class AbstractSyncEntity extends AbstractEntity implements |
76: | SyncEntityInterface, |
77: | Flushable |
78: | { |
79: | use ConstructibleTrait; |
80: | use HasReadableProperties; |
81: | use HasWritableProperties; |
82: | use ExtensibleTrait { |
83: | ExtensibleTrait::__set insteadof HasWritableProperties; |
84: | ExtensibleTrait::__get insteadof HasReadableProperties; |
85: | ExtensibleTrait::__isset insteadof HasReadableProperties; |
86: | ExtensibleTrait::__unset insteadof HasWritableProperties; |
87: | } |
88: | |
89: | use ProvidableTrait; |
90: | use HasNormaliser; |
91: | use RequiresContainer; |
92: | |
93: | |
94: | |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | public $Id; |
101: | |
102: | |
103: | |
104: | |
105: | |
106: | |
107: | |
108: | |
109: | public $CanonicalId; |
110: | |
111: | |
112: | private $Provider; |
113: | |
114: | private $Context; |
115: | |
116: | private $State = 0; |
117: | |
118: | |
119: | |
120: | |
121: | |
122: | |
123: | private static array $RemovablePrefixRegex = []; |
124: | |
125: | |
126: | |
127: | |
128: | |
129: | |
130: | private static array $NormalisedPropertyMap = []; |
131: | |
132: | |
133: | |
134: | |
135: | public static function getPlural(): ?string |
136: | { |
137: | return Inflect::plural(Get::basename(static::class)); |
138: | } |
139: | |
140: | |
141: | |
142: | |
143: | public static function getRelationships(): array |
144: | { |
145: | return []; |
146: | } |
147: | |
148: | |
149: | |
150: | |
151: | public static function getDateProperties(): array |
152: | { |
153: | return []; |
154: | } |
155: | |
156: | |
157: | |
158: | |
159: | public static function flushStatic(): void |
160: | { |
161: | unset( |
162: | self::$RemovablePrefixRegex[static::class], |
163: | self::$NormalisedPropertyMap[static::class], |
164: | ); |
165: | } |
166: | |
167: | |
168: | |
169: | |
170: | public function getName(): string |
171: | { |
172: | return |
173: | SyncIntrospector::get(static::class) |
174: | ->getGetNameClosure()($this); |
175: | } |
176: | |
177: | |
178: | |
179: | |
180: | |
181: | |
182: | |
183: | |
184: | |
185: | |
186: | |
187: | |
188: | protected static function buildSerializeRules(SyncSerializeRulesBuilder $rulesB): SyncSerializeRulesBuilder |
189: | { |
190: | return $rulesB; |
191: | } |
192: | |
193: | |
194: | |
195: | |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | |
206: | |
207: | |
208: | |
209: | protected static function getRemovablePrefixes(): ?array |
210: | { |
211: | $current = new ReflectionClass(static::class); |
212: | do { |
213: | $prefixes[] = Get::basename($current->getName()); |
214: | $current = $current->getParentClass(); |
215: | } while ($current && $current->isSubclassOf(self::class)); |
216: | |
217: | return self::expandPrefixes($prefixes); |
218: | } |
219: | |
220: | |
221: | |
222: | |
223: | |
224: | |
225: | final public function getId() |
226: | { |
227: | return $this->Id; |
228: | } |
229: | |
230: | |
231: | |
232: | |
233: | final public function getCanonicalId() |
234: | { |
235: | return $this->CanonicalId; |
236: | } |
237: | |
238: | |
239: | |
240: | |
241: | final public static function getDefaultProvider(ContainerInterface $container): SyncProviderInterface |
242: | { |
243: | return $container->get(SyncUtil::getEntityTypeProvider(static::class, SyncUtil::getStore($container))); |
244: | } |
245: | |
246: | |
247: | |
248: | |
249: | final public static function withDefaultProvider(ContainerInterface $container, ?SyncContextInterface $context = null): SyncEntityProviderInterface |
250: | { |
251: | return static::getDefaultProvider($container)->with(static::class, $context); |
252: | } |
253: | |
254: | |
255: | |
256: | |
257: | final public static function getSerializeRules(): SyncSerializeRulesInterface |
258: | { |
259: | return static::buildSerializeRules( |
260: | SyncSerializeRules::build() |
261: | ->entity(static::class) |
262: | )->build(); |
263: | } |
264: | |
265: | |
266: | |
267: | |
268: | |
269: | |
270: | final protected static function getParentSerializeRules(): ?SyncSerializeRules |
271: | { |
272: | $class = get_parent_class(get_called_class()); |
273: | |
274: | if ( |
275: | $class === false |
276: | || !is_a($class, self::class, true) |
277: | || $class === self::class |
278: | ) { |
279: | return null; |
280: | } |
281: | |
282: | |
283: | return $class::buildSerializeRules( |
284: | SyncSerializeRules::build() |
285: | ->entity($class) |
286: | )->build(); |
287: | } |
288: | |
289: | |
290: | |
291: | |
292: | |
293: | |
294: | |
295: | |
296: | |
297: | final public static function normaliseProperty( |
298: | string $name, |
299: | bool $greedy = true, |
300: | string ...$hints |
301: | ): string { |
302: | $regex = self::$RemovablePrefixRegex[static::class] |
303: | ??= self::getRemovablePrefixRegex(); |
304: | |
305: | if ($regex === false) { |
306: | return self::$NormalisedPropertyMap[static::class][$name] |
307: | ??= Str::snake($name); |
308: | } |
309: | |
310: | if ($greedy && !$hints) { |
311: | return self::$NormalisedPropertyMap[static::class][$name] |
312: | ??= Regex::replace($regex, '', Str::snake($name)); |
313: | } |
314: | |
315: | $_name = Str::snake($name); |
316: | if (!$greedy || in_array($_name, $hints)) { |
317: | return $_name; |
318: | } |
319: | |
320: | return Regex::replace($regex, '', $_name); |
321: | } |
322: | |
323: | |
324: | |
325: | |
326: | private static function getRemovablePrefixRegex() |
327: | { |
328: | $prefixes = static::getRemovablePrefixes(); |
329: | |
330: | return $prefixes |
331: | ? sprintf( |
332: | count($prefixes) > 1 ? '/^(?:%s)_/' : '/^%s_/', |
333: | implode('|', $prefixes), |
334: | ) |
335: | : false; |
336: | } |
337: | |
338: | |
339: | |
340: | |
341: | final public function toArray(?SyncStoreInterface $store = null): array |
342: | { |
343: | |
344: | $rules = static::getSerializeRules(); |
345: | return $this->_toArray($rules, $store); |
346: | } |
347: | |
348: | |
349: | |
350: | |
351: | final public function toArrayWith(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store = null): array |
352: | { |
353: | |
354: | return $this->_toArray($rules, $store); |
355: | } |
356: | |
357: | |
358: | |
359: | |
360: | final public function toLink(?SyncStoreInterface $store = null, int $type = LinkType::DEFAULT, bool $compact = true): array |
361: | { |
362: | switch ($type) { |
363: | case LinkType::DEFAULT: |
364: | return [ |
365: | '@type' => $this->getTypeUri($store, $compact), |
366: | '@id' => $this->Id === null |
367: | ? spl_object_id($this) |
368: | : $this->Id, |
369: | ]; |
370: | |
371: | case LinkType::COMPACT: |
372: | return [ |
373: | '@id' => $this->getUri($store, $compact), |
374: | ]; |
375: | |
376: | case LinkType::FRIENDLY: |
377: | return Arr::whereNotEmpty([ |
378: | '@type' => $this->getTypeUri($store, $compact), |
379: | '@id' => $this->Id === null |
380: | ? spl_object_id($this) |
381: | : $this->Id, |
382: | '@name' => $this->getName(), |
383: | '@description' => |
384: | $this instanceof HasDescription |
385: | ? $this->getDescription() |
386: | : null, |
387: | ]); |
388: | |
389: | default: |
390: | throw new LogicException("Invalid link type: $type"); |
391: | } |
392: | } |
393: | |
394: | |
395: | |
396: | |
397: | final public function getUri(?SyncStoreInterface $store = null, bool $compact = true): string |
398: | { |
399: | return sprintf( |
400: | '%s/%s', |
401: | $this->getTypeUri($store, $compact), |
402: | $this->Id === null |
403: | ? spl_object_id($this) |
404: | : $this->Id |
405: | ); |
406: | } |
407: | |
408: | |
409: | |
410: | |
411: | |
412: | |
413: | final public function state(): int |
414: | { |
415: | return $this->State; |
416: | } |
417: | |
418: | |
419: | |
420: | |
421: | |
422: | |
423: | |
424: | |
425: | |
426: | final protected static function normalisePrefixes(array $prefixes): array |
427: | { |
428: | if (!$prefixes) { |
429: | return []; |
430: | } |
431: | |
432: | return Arr::snakeCase($prefixes); |
433: | } |
434: | |
435: | |
436: | |
437: | |
438: | |
439: | |
440: | |
441: | |
442: | |
443: | |
444: | |
445: | final protected static function expandPrefixes(array $prefixes): array |
446: | { |
447: | if (!$prefixes) { |
448: | return []; |
449: | } |
450: | |
451: | foreach ($prefixes as $prefix) { |
452: | $prefix = Str::snake($prefix); |
453: | $expanded[$prefix] = true; |
454: | $prefix = explode('_', $prefix); |
455: | while (array_shift($prefix)) { |
456: | $expanded[implode('_', $prefix)] = true; |
457: | } |
458: | } |
459: | |
460: | return array_keys($expanded); |
461: | } |
462: | |
463: | private function getTypeUri(?SyncStoreInterface $store, bool $compact): string |
464: | { |
465: | |
466: | $service = $this->getService(); |
467: | $store ??= $this->Provider ? $this->Provider->getStore() : null; |
468: | return SyncUtil::getEntityTypeUri($service, $compact, $store); |
469: | } |
470: | |
471: | |
472: | |
473: | |
474: | |
475: | private function _toArray(SyncSerializeRulesInterface $rules, ?SyncStoreInterface $store): array |
476: | { |
477: | |
478: | if ($rules->getDateFormatter() === null) { |
479: | $rules = $rules->withDateFormatter( |
480: | $this->Provider |
481: | ? $this->Provider->getDateFormatter() |
482: | : new DateFormatter() |
483: | ); |
484: | } |
485: | |
486: | $array = $this; |
487: | $this->_serialize($array, [], $rules, $store); |
488: | |
489: | return (array) $array; |
490: | } |
491: | |
492: | |
493: | |
494: | |
495: | |
496: | |
497: | |
498: | private function _serialize( |
499: | &$node, |
500: | array $path, |
501: | SyncSerializeRulesInterface $rules, |
502: | ?SyncStoreInterface $store, |
503: | bool $nodeIsList = false, |
504: | array $parents = [] |
505: | ): void { |
506: | $maxDepth = $rules->getMaxDepth(); |
507: | if ($maxDepth !== null && count($path) > $maxDepth) { |
508: | throw new UnexpectedValueException('In too deep: ' . implode('.', $path)); |
509: | } |
510: | |
511: | if ($node instanceof DateTimeInterface) { |
512: | |
513: | $formatter = $rules->getDateFormatter(); |
514: | $node = $formatter->format($node); |
515: | return; |
516: | } |
517: | |
518: | |
519: | if ($node instanceof DeferredEntityInterface) { |
520: | $node = $node->toLink(LinkType::DEFAULT); |
521: | return; |
522: | } |
523: | |
524: | |
525: | if ($node instanceof DeferredRelationshipInterface) { |
526: | $node = null; |
527: | return; |
528: | } |
529: | |
530: | if ($node instanceof AbstractSyncEntity) { |
531: | if ($path && $rules->getForSyncStore()) { |
532: | $node = $node->toLink($store, LinkType::DEFAULT); |
533: | return; |
534: | } |
535: | |
536: | if ($rules->getDetectRecursion()) { |
537: | |
538: | |
539: | if ($parents[spl_object_id($node)] ?? false) { |
540: | $node = $node->toLink($store, LinkType::DEFAULT); |
541: | $node['@why'] = 'Circular reference detected'; |
542: | return; |
543: | } |
544: | $parents[spl_object_id($node)] = true; |
545: | } |
546: | |
547: | $class = get_class($node); |
548: | $node = $node->serialize($rules); |
549: | } |
550: | |
551: | $delete = $rules->getRemovableKeys($class ?? null, null, $path); |
552: | $replace = $rules->getReplaceableKeys($class ?? null, null, $path); |
553: | |
554: | |
555: | $delete = array_diff_key($delete, $replace); |
556: | |
557: | if ($delete) { |
558: | $node = array_diff_key($node, $delete); |
559: | } |
560: | |
561: | $replace = array_intersect_key($replace, $node + ['[]' => null]); |
562: | |
563: | foreach ($replace as $key => [$newKey, $closure]) { |
564: | if ($key !== '[]') { |
565: | if ($newKey !== null && $key !== $newKey) { |
566: | $node = Arr::rename($node, $key, $newKey); |
567: | $key = $newKey; |
568: | } |
569: | |
570: | if ($closure) { |
571: | if ($node[$key] !== null) { |
572: | $node[$key] = $closure($node[$key], $store); |
573: | } |
574: | continue; |
575: | } |
576: | |
577: | $_path = $path; |
578: | $_path[] = (string) $key; |
579: | $this->_serializeId($node[$key], $_path); |
580: | continue; |
581: | } |
582: | |
583: | if (!$nodeIsList) { |
584: | continue; |
585: | } |
586: | |
587: | if ($closure) { |
588: | foreach ($node as &$child) { |
589: | if ($child !== null) { |
590: | $child = $closure($child, $store); |
591: | } |
592: | } |
593: | unset($child); |
594: | continue; |
595: | } |
596: | |
597: | $_path = $path; |
598: | $last = array_pop($_path) . '[]'; |
599: | $_path[] = $last; |
600: | foreach ($node as &$child) { |
601: | $this->_serializeId($child, $_path); |
602: | } |
603: | unset($child); |
604: | } |
605: | |
606: | $isList = false; |
607: | if (Arr::isIndexed($node)) { |
608: | $isList = true; |
609: | $last = array_pop($path) . '[]'; |
610: | $path[] = $last; |
611: | } |
612: | |
613: | unset($_path); |
614: | foreach ($node as $key => &$child) { |
615: | if ($child === null || is_scalar($child)) { |
616: | continue; |
617: | } |
618: | if (!$isList) { |
619: | $_path = $path; |
620: | $_path[] = (string) $key; |
621: | } |
622: | $this->_serialize( |
623: | $child, |
624: | $_path ?? $path, |
625: | $rules, |
626: | $store, |
627: | $isList, |
628: | $parents, |
629: | ); |
630: | } |
631: | } |
632: | |
633: | |
634: | |
635: | |
636: | |
637: | private function _serializeId(&$node, array $path): void |
638: | { |
639: | if ($node === null) { |
640: | return; |
641: | } |
642: | |
643: | if (Arr::of($node, AbstractSyncEntity::class, true) && Arr::isIndexed($node, true)) { |
644: | |
645: | foreach ($node as &$child) { |
646: | $child = $child->Id; |
647: | } |
648: | |
649: | return; |
650: | } |
651: | |
652: | if (!$node instanceof AbstractSyncEntity) { |
653: | throw new UnexpectedValueException('Cannot replace (not an AbstractSyncEntity): ' . implode('.', $path)); |
654: | } |
655: | |
656: | $node = $node->Id; |
657: | } |
658: | |
659: | |
660: | |
661: | |
662: | |
663: | |
664: | |
665: | |
666: | |
667: | |
668: | private function serialize(SyncSerializeRulesInterface $rules): array |
669: | { |
670: | $clone = clone $this; |
671: | $clone->State |= EntityState::SERIALIZING; |
672: | $array = SyncIntrospector::get(static::class) |
673: | ->getSerializeClosure($rules)($clone); |
674: | |
675: | if (!$rules->getIncludeCanonicalId()) { |
676: | unset($array[ |
677: | SyncIntrospector::get(static::class) |
678: | ->maybeNormalise('CanonicalId', NormaliserFlag::CAREFUL) |
679: | ]); |
680: | } |
681: | |
682: | return $array; |
683: | } |
684: | |
685: | |
686: | |
687: | |
688: | |
689: | |
690: | final public function jsonSerialize(): array |
691: | { |
692: | return $this->toArray(); |
693: | } |
694: | |
695: | |
696: | |
697: | |
698: | |
699: | final public static function provide( |
700: | array $data, |
701: | ProviderInterface $provider, |
702: | ?ProviderContextInterface $context = null |
703: | ) { |
704: | $container = $context |
705: | ? $context->getContainer() |
706: | : $provider->getContainer(); |
707: | $container = $container->inContextOf(get_class($provider)); |
708: | |
709: | $context = ($context ?? $provider->getContext())->withContainer($container); |
710: | |
711: | $closure = SyncIntrospector::getService( |
712: | $container, static::class |
713: | )->getCreateSyncEntityFromClosure(); |
714: | |
715: | return $closure($data, $provider, $context); |
716: | } |
717: | |
718: | |
719: | |
720: | |
721: | |
722: | final public static function provideMultiple( |
723: | iterable $list, |
724: | ProviderInterface $provider, |
725: | int $conformity = ListConformity::NONE, |
726: | ?ProviderContextInterface $context = null |
727: | ): FluentIteratorInterface { |
728: | return IterableIterator::from( |
729: | self::_provideMultiple($list, $provider, $conformity, $context) |
730: | ); |
731: | } |
732: | |
733: | |
734: | |
735: | |
736: | final public static function idFromNameOrId( |
737: | $nameOrId, |
738: | $providerOrContext, |
739: | ?float $uncertaintyThreshold = null, |
740: | ?string $nameProperty = null, |
741: | ?float &$uncertainty = null |
742: | ) { |
743: | if ($nameOrId === null) { |
744: | $uncertainty = null; |
745: | return null; |
746: | } |
747: | |
748: | if ($providerOrContext instanceof SyncProviderInterface) { |
749: | $provider = $providerOrContext; |
750: | $context = $provider->getContext(); |
751: | } else { |
752: | $context = $providerOrContext; |
753: | $provider = $context->getProvider(); |
754: | } |
755: | |
756: | if ($provider->isValidIdentifier($nameOrId, static::class)) { |
757: | $uncertainty = 0.0; |
758: | return $nameOrId; |
759: | } |
760: | |
761: | $entity = $provider |
762: | ->with(static::class, $context) |
763: | ->getResolver( |
764: | $nameProperty, |
765: | Algorithm::SAME | Algorithm::CONTAINS | Algorithm::NGRAM_SIMILARITY | Flag::NORMALISE, |
766: | $uncertaintyThreshold, |
767: | null, |
768: | true, |
769: | ) |
770: | ->getByName((string) $nameOrId, $uncertainty); |
771: | |
772: | if ($entity) { |
773: | return $entity->Id; |
774: | } |
775: | |
776: | throw new SyncEntityNotFoundException( |
777: | $provider, |
778: | static::class, |
779: | $nameProperty === null |
780: | ? ['name' => $nameOrId] |
781: | : [$nameProperty => $nameOrId], |
782: | ); |
783: | } |
784: | |
785: | |
786: | |
787: | |
788: | public function postLoad(): void {} |
789: | |
790: | |
791: | |
792: | |
793: | public function __serialize(): array |
794: | { |
795: | foreach ([ |
796: | ...SyncIntrospector::get(static::class)->SerializableProperties, |
797: | 'MetaProperties', |
798: | 'MetaPropertyNames', |
799: | ] as $property) { |
800: | $data[$property] = $this->{$property}; |
801: | } |
802: | |
803: | $data['Provider'] = $this->Provider === null |
804: | ? null |
805: | : $this->Provider->getStore()->getProviderSignature($this->Provider); |
806: | |
807: | return $data; |
808: | } |
809: | |
810: | |
811: | |
812: | |
813: | public function __unserialize(array $data): void |
814: | { |
815: | foreach ($data as $property => $value) { |
816: | if ($property === 'Provider' && $value !== null) { |
817: | $value = is_string($value) && Sync::isLoaded() && Sync::hasProvider($value) |
818: | ? Sync::getProvider($value) |
819: | : null; |
820: | if ($value === null) { |
821: | throw new UnexpectedValueException('Cannot unserialize missing provider'); |
822: | } |
823: | } |
824: | $this->{$property} = $value; |
825: | } |
826: | } |
827: | |
828: | |
829: | |
830: | |
831: | |
832: | |
833: | |
834: | |
835: | private static function _provideMultiple( |
836: | iterable $list, |
837: | ProviderInterface $provider, |
838: | $conformity, |
839: | ?ProviderContextInterface $context |
840: | ): Generator { |
841: | $container = $context |
842: | ? $context->getContainer() |
843: | : $provider->getContainer(); |
844: | $container = $container->inContextOf(get_class($provider)); |
845: | |
846: | $conformity = $context |
847: | ? max($context->getConformity(), $conformity) |
848: | : $conformity; |
849: | |
850: | $context = ($context ?? $provider->getContext())->withContainer($container); |
851: | |
852: | $introspector = SyncIntrospector::getService($container, static::class); |
853: | |
854: | foreach ($list as $key => $data) { |
855: | if (!isset($closure)) { |
856: | $closure = $conformity === ListConformity::PARTIAL || $conformity === ListConformity::COMPLETE |
857: | ? $introspector->getCreateSyncEntityFromSignatureClosure(array_keys($data)) |
858: | : $introspector->getCreateSyncEntityFromClosure(); |
859: | } |
860: | |
861: | yield $key => $closure($data, $provider, $context); |
862: | } |
863: | } |
864: | } |
865: | |