001/*
002 * Copyright (c) 2006-2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Bogdan Stefanescu
011 *     Florent Guillaume
012 */
013
014package org.nuxeo.ecm.core.schema;
015
016import java.io.File;
017import java.io.IOException;
018import java.io.InputStream;
019import java.net.URL;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.LinkedHashMap;
027import java.util.LinkedHashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import java.util.concurrent.ConcurrentHashMap;
032
033import org.apache.commons.lang.StringUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036
037import org.nuxeo.common.utils.FileUtils;
038import org.nuxeo.ecm.core.schema.types.AnyType;
039import org.nuxeo.ecm.core.schema.types.ComplexType;
040import org.nuxeo.ecm.core.schema.types.CompositeType;
041import org.nuxeo.ecm.core.schema.types.CompositeTypeImpl;
042import org.nuxeo.ecm.core.schema.types.Field;
043import org.nuxeo.ecm.core.schema.types.ListType;
044import org.nuxeo.ecm.core.schema.types.QName;
045import org.nuxeo.ecm.core.schema.types.Schema;
046import org.nuxeo.ecm.core.schema.types.Type;
047import org.nuxeo.ecm.core.schema.types.TypeException;
048import org.xml.sax.SAXException;
049
050/**
051 * Schema Manager implementation.
052 * <p>
053 * Holds basic types (String, Integer, etc.), schemas, document types and facets.
054 */
055public class SchemaManagerImpl implements SchemaManager {
056
057    private static final Log log = LogFactory.getLog(SchemaManagerImpl.class);
058
059    /**
060     * Whether there have been changes to the registered schemas, facets or document types that require recomputation of
061     * the effective ones.
062     */
063    // volatile to use double-check idiom
064    protected volatile boolean dirty = true;
065
066    /** Basic type registry. */
067    protected Map<String, Type> types = new HashMap<>();
068
069    /** All the registered configurations (prefetch). */
070    protected List<TypeConfiguration> allConfigurations = new ArrayList<>();
071
072    /** All the registered schemas. */
073    protected List<SchemaBindingDescriptor> allSchemas = new ArrayList<>();
074
075    /** All the registered facets. */
076    protected List<FacetDescriptor> allFacets = new ArrayList<>();
077
078    /** All the registered document types. */
079    protected List<DocumentTypeDescriptor> allDocumentTypes = new ArrayList<>();
080
081    /** All the registered proxy descriptors. */
082    protected List<ProxiesDescriptor> allProxies = new ArrayList<>();
083
084    /** Effective prefetch info. */
085    protected PrefetchInfo prefetchInfo;
086
087    /** Effective schemas. */
088    protected Map<String, Schema> schemas = new HashMap<>();
089
090    protected final Map<String, Schema> uriToSchema = new HashMap<>();
091
092    protected final Map<String, Schema> prefixToSchema = new HashMap<>();
093
094    /** Effective facets. */
095    protected Map<String, CompositeType> facets = new HashMap<>();
096
097    protected Set<String> noPerDocumentQueryFacets = new HashSet<>();
098
099    /** Effective document types. */
100    protected Map<String, DocumentTypeImpl> documentTypes = new HashMap<>();
101
102    protected Map<String, Set<String>> documentTypesExtending = new HashMap<>();
103
104    protected Map<String, Set<String>> documentTypesForFacet = new HashMap<>();
105
106    /** Effective proxy schemas. */
107    protected List<Schema> proxySchemas = new ArrayList<>();
108
109    /** Effective proxy schema names. */
110    protected Set<String> proxySchemaNames = new HashSet<>();
111
112    /** Fields computed lazily. */
113    private Map<String, Field> fields = new ConcurrentHashMap<>();
114
115    private File schemaDir;
116
117    public static final String SCHEMAS_DIR_NAME = "schemas";
118
119    public SchemaManagerImpl() {
120        schemaDir = new File(System.getProperty("java.io.tmpdir"), SCHEMAS_DIR_NAME);
121        if (!schemaDir.isDirectory()) {
122            schemaDir.mkdirs();
123        }
124        clearSchemaDir();
125        registerBuiltinTypes();
126    }
127
128    protected void clearSchemaDir() {
129        try {
130            org.apache.commons.io.FileUtils.cleanDirectory(schemaDir);
131        } catch (IOException e) {
132            throw new RuntimeException(e);
133        }
134    }
135
136    public File getSchemasDir() {
137        return schemaDir;
138    }
139
140    protected void registerBuiltinTypes() {
141        for (Type type : XSDTypes.getTypes()) {
142            registerType(type);
143        }
144        registerType(AnyType.INSTANCE);
145    }
146
147    protected void registerType(Type type) {
148        types.put(type.getName(), type);
149    }
150
151    // called by XSDLoader
152    protected Type getType(String name) {
153        return types.get(name);
154    }
155
156    // for tests
157    protected Collection<Type> getTypes() {
158        return types.values();
159    }
160
161    public synchronized void registerConfiguration(TypeConfiguration config) {
162        allConfigurations.add(config);
163        dirty = true;
164        log.info("Registered global prefetch: " + config.prefetchInfo);
165    }
166
167    public synchronized void unregisterConfiguration(TypeConfiguration config) {
168        if (allConfigurations.remove(config)) {
169            dirty = true;
170            log.info("Unregistered global prefetch: " + config.prefetchInfo);
171        } else {
172            log.error("Unregistering unknown prefetch: " + config.prefetchInfo);
173
174        }
175    }
176
177    public synchronized void registerSchema(SchemaBindingDescriptor sd) {
178        allSchemas.add(sd);
179        dirty = true;
180        log.info("Registered schema: " + sd.name);
181    }
182
183    public synchronized void unregisterSchema(SchemaBindingDescriptor sd) {
184        if (allSchemas.remove(sd)) {
185            dirty = true;
186            log.info("Unregistered schema: " + sd.name);
187        } else {
188            log.error("Unregistering unknown schema: " + sd.name);
189        }
190    }
191
192    public synchronized void registerFacet(FacetDescriptor fd) {
193        allFacets.add(fd);
194        dirty = true;
195        log.info("Registered facet: " + fd.name);
196    }
197
198    public synchronized void unregisterFacet(FacetDescriptor fd) {
199        if (allFacets.remove(fd)) {
200            dirty = true;
201            log.info("Unregistered facet: " + fd.name);
202        } else {
203            log.error("Unregistering unknown facet: " + fd.name);
204        }
205    }
206
207    public synchronized void registerDocumentType(DocumentTypeDescriptor dtd) {
208        allDocumentTypes.add(dtd);
209        dirty = true;
210        log.info("Registered document type: " + dtd.name);
211    }
212
213    public synchronized void unregisterDocumentType(DocumentTypeDescriptor dtd) {
214        if (allDocumentTypes.remove(dtd)) {
215            dirty = true;
216            log.info("Unregistered document type: " + dtd.name);
217        } else {
218            log.error("Unregistering unknown document type: " + dtd.name);
219        }
220    }
221
222    // for tests
223    public DocumentTypeDescriptor getDocumentTypeDescriptor(String name) {
224        DocumentTypeDescriptor last = null;
225        for (DocumentTypeDescriptor dtd : allDocumentTypes) {
226            if (dtd.name.equals(name)) {
227                last = dtd;
228            }
229        }
230        return last;
231    }
232
233    public synchronized void registerProxies(ProxiesDescriptor pd) {
234        allProxies.add(pd);
235        dirty = true;
236        log.info("Registered proxies descriptor for schemas: " + pd.getSchemas());
237    }
238
239    public synchronized void unregisterProxies(ProxiesDescriptor pd) {
240        if (allProxies.remove(pd)) {
241            dirty = true;
242            log.info("Unregistered proxies descriptor for schemas: " + pd.getSchemas());
243        } else {
244            log.error("Unregistering unknown proxies descriptor for schemas: " + pd.getSchemas());
245        }
246    }
247
248    /**
249     * Checks if something has to be recomputed if a dynamic register/unregister happened.
250     */
251    protected void checkDirty() {
252        // variant of double-check idiom
253        if (!dirty) {
254            return;
255        }
256        synchronized (this) {
257            if (!dirty) {
258                return;
259            }
260            // call recompute() synchronized
261            recompute();
262            dirty = false;
263        }
264    }
265
266    /**
267     * Recomputes effective registries for schemas, facets and document types.
268     */
269    protected void recompute() {
270        recomputeConfiguration();
271        recomputeSchemas();
272        recomputeFacets(); // depend on schemas
273        recomputeDocumentTypes(); // depend on schemas and facets
274        recomputeProxies(); // depend on schemas
275        fields.clear(); // re-filled lazily
276    }
277
278    /*
279     * ===== Configuration =====
280     */
281
282    protected void recomputeConfiguration() {
283        if (allConfigurations.isEmpty()) {
284            prefetchInfo = null;
285        } else {
286            TypeConfiguration last = allConfigurations.get(allConfigurations.size() - 1);
287            prefetchInfo = new PrefetchInfo(last.prefetchInfo);
288        }
289    }
290
291    /*
292     * ===== Schemas =====
293     */
294
295    protected void recomputeSchemas() {
296        schemas.clear();
297        uriToSchema.clear();
298        prefixToSchema.clear();
299        RuntimeException errors = new RuntimeException("Cannot load schemas");
300        // on reload, don't take confuse already-copied schemas with those contributed
301        clearSchemaDir();
302        // resolve which schemas to actually load depending on overrides
303        Map<String, SchemaBindingDescriptor> resolvedSchemas = new LinkedHashMap<>();
304        for (SchemaBindingDescriptor sd : allSchemas) {
305            String name = sd.name;
306            if (resolvedSchemas.containsKey(name)) {
307                if (!sd.override) {
308                    log.warn("Schema " + name + " is redefined but will not be overridden");
309                    continue;
310                }
311                log.debug("Reregistering schema: " + name + " from " + sd.file);
312            } else {
313                log.debug("Registering schema: " + name + " from " + sd.file);
314            }
315            resolvedSchemas.put(name, sd);
316        }
317        for (SchemaBindingDescriptor sd : resolvedSchemas.values()) {
318            try {
319                copySchema(sd);
320            } catch (IOException error) {
321                errors.addSuppressed(error);
322            }
323        }
324        for (SchemaBindingDescriptor sd : resolvedSchemas.values()) {
325            try {
326                loadSchema(sd);
327            } catch (IOException | SAXException | TypeException error) {
328                errors.addSuppressed(error);
329            }
330        }
331        if (errors.getSuppressed().length > 0) {
332            throw errors;
333        }
334    }
335
336    protected void copySchema(SchemaBindingDescriptor sd) throws IOException {
337        if (sd.src == null || sd.src.length() == 0) {
338            // log.error("INLINE Schemas ARE NOT YET IMPLEMENTED!");
339            return;
340        }
341        URL url = sd.context.getLocalResource(sd.src);
342        if (url == null) {
343            // try asking the class loader
344            url = sd.context.getResource(sd.src);
345        }
346        if (url == null) {
347            log.error("XSD Schema not found: " + sd.src);
348            return;
349        }
350        InputStream in = url.openStream();
351        try {
352            sd.file = new File(schemaDir, sd.name + ".xsd");
353            FileUtils.copyToFile(in, sd.file); // may overwrite
354        } finally {
355            in.close();
356        }
357    }
358
359    protected void loadSchema(SchemaBindingDescriptor sd) throws IOException, SAXException, TypeException {
360        if (sd.file == null) {
361            // log.error("INLINE Schemas ARE NOT YET IMPLEMENTED!");
362            return;
363        }
364        // loadSchema calls this.registerSchema
365        XSDLoader schemaLoader = new XSDLoader(this, sd);
366        schemaLoader.loadSchema(sd.name, sd.prefix, sd.file, sd.xsdRootElement);
367        log.info("Registered schema: " + sd.name + " from " + sd.file);
368    }
369
370    // called from XSDLoader
371    protected void registerSchema(Schema schema) {
372        schemas.put(schema.getName(), schema);
373        Namespace ns = schema.getNamespace();
374        uriToSchema.put(ns.uri, schema);
375        if (!StringUtils.isBlank(ns.prefix)) {
376            prefixToSchema.put(ns.prefix, schema);
377        }
378    }
379
380    @Override
381    public Schema[] getSchemas() {
382        checkDirty();
383        return new ArrayList<>(schemas.values()).toArray(new Schema[0]);
384    }
385
386    @Override
387    public Schema getSchema(String name) {
388        checkDirty();
389        return schemas.get(name);
390    }
391
392    @Override
393    public Schema getSchemaFromPrefix(String schemaPrefix) {
394        checkDirty();
395        return prefixToSchema.get(schemaPrefix);
396    }
397
398    @Override
399    public Schema getSchemaFromURI(String schemaURI) {
400        checkDirty();
401        return uriToSchema.get(schemaURI);
402    }
403
404    /*
405     * ===== Facets =====
406     */
407
408    protected void recomputeFacets() {
409        facets.clear();
410        noPerDocumentQueryFacets.clear();
411        for (FacetDescriptor fd : allFacets) {
412            recomputeFacet(fd);
413        }
414    }
415
416    protected void recomputeFacet(FacetDescriptor fd) {
417        Set<String> fdSchemas = SchemaDescriptor.getSchemaNames(fd.schemas);
418        registerFacet(fd.name, fdSchemas);
419        if (Boolean.FALSE.equals(fd.perDocumentQuery)) {
420            noPerDocumentQueryFacets.add(fd.name);
421        }
422    }
423
424    // also called when a document type references an unknown facet (WARN)
425    protected CompositeType registerFacet(String name, Set<String> schemaNames) {
426        List<Schema> facetSchemas = new ArrayList<>(schemaNames.size());
427        for (String schemaName : schemaNames) {
428            Schema schema = schemas.get(schemaName);
429            if (schema == null) {
430                log.error("Facet: " + name + " uses unknown schema: " + schemaName);
431                continue;
432            }
433            facetSchemas.add(schema);
434        }
435        CompositeType ct = new CompositeTypeImpl(null, SchemaNames.FACETS, name, facetSchemas);
436        facets.put(name, ct);
437        return ct;
438    }
439
440    @Override
441    public CompositeType[] getFacets() {
442        checkDirty();
443        return new ArrayList<>(facets.values()).toArray(new CompositeType[facets.size()]);
444    }
445
446    @Override
447    public CompositeType getFacet(String name) {
448        checkDirty();
449        return facets.get(name);
450    }
451
452    @Override
453    public Set<String> getNoPerDocumentQueryFacets() {
454        checkDirty();
455        return Collections.unmodifiableSet(noPerDocumentQueryFacets);
456    }
457
458    /*
459     * ===== Document types =====
460     */
461
462    protected void recomputeDocumentTypes() {
463        // effective descriptors with override
464        // linked hash map to keep order for reproducibility
465        Map<String, DocumentTypeDescriptor> dtds = new LinkedHashMap<>();
466        for (DocumentTypeDescriptor dtd : allDocumentTypes) {
467            String name = dtd.name;
468            DocumentTypeDescriptor newDtd = dtd;
469            if (dtd.append && dtds.containsKey(dtd.name)) {
470                newDtd = mergeDocumentTypeDescriptors(dtd, dtds.get(name));
471            }
472            dtds.put(name, newDtd);
473        }
474        // recompute all types, parents first
475        documentTypes.clear();
476        documentTypesExtending.clear();
477        registerDocumentType(new DocumentTypeImpl(TypeConstants.DOCUMENT)); // Document
478        for (String name : dtds.keySet()) {
479            LinkedHashSet<String> stack = new LinkedHashSet<>();
480            recomputeDocumentType(name, stack, dtds);
481        }
482
483        // document types having a given facet
484        documentTypesForFacet.clear();
485        for (DocumentType docType : documentTypes.values()) {
486            for (String facet : docType.getFacets()) {
487                Set<String> set = documentTypesForFacet.get(facet);
488                if (set == null) {
489                    documentTypesForFacet.put(facet, set = new HashSet<>());
490                }
491                set.add(docType.getName());
492            }
493        }
494
495    }
496
497    protected DocumentTypeDescriptor mergeDocumentTypeDescriptors(DocumentTypeDescriptor src, DocumentTypeDescriptor dst) {
498        return dst.clone().merge(src);
499    }
500
501    protected DocumentType recomputeDocumentType(String name, Set<String> stack,
502            Map<String, DocumentTypeDescriptor> dtds) {
503        DocumentTypeImpl docType = documentTypes.get(name);
504        if (docType != null) {
505            // already done
506            return docType;
507        }
508        if (stack.contains(name)) {
509            log.error("Document type: " + name + " used in parent inheritance loop: " + stack);
510            return null;
511        }
512        DocumentTypeDescriptor dtd = dtds.get(name);
513        if (dtd == null) {
514            log.error("Document type: " + name + " does not exist, used as parent by type: " + stack);
515            return null;
516        }
517
518        // find and recompute the parent first
519        DocumentType parent;
520        String parentName = dtd.superTypeName;
521        if (parentName == null) {
522            parent = null;
523        } else {
524            parent = documentTypes.get(parentName);
525            if (parent == null) {
526                stack.add(name);
527                parent = recomputeDocumentType(parentName, stack, dtds);
528                stack.remove(name);
529            }
530        }
531
532        // what it extends
533        for (Type p = parent; p != null; p = p.getSuperType()) {
534            Set<String> set = documentTypesExtending.get(p.getName());
535            set.add(name);
536        }
537
538        return recomputeDocumentType(name, dtd, parent);
539    }
540
541    protected DocumentType recomputeDocumentType(String name, DocumentTypeDescriptor dtd, DocumentType parent) {
542        // find the facets and schemas names
543        Set<String> facetNames = new HashSet<>();
544        Set<String> schemaNames = SchemaDescriptor.getSchemaNames(dtd.schemas);
545        facetNames.addAll(Arrays.asList(dtd.facets));
546
547        // inherited
548        if (parent != null) {
549            facetNames.addAll(parent.getFacets());
550            schemaNames.addAll(Arrays.asList(parent.getSchemaNames()));
551        }
552
553        // add schemas names from facets
554        for (String facetName : facetNames) {
555            CompositeType ct = facets.get(facetName);
556            if (ct == null) {
557                log.warn("Undeclared facet: " + facetName + " used in document type: " + name);
558                // register it with no schemas
559                ct = registerFacet(facetName, Collections.<String> emptySet());
560            }
561            schemaNames.addAll(Arrays.asList(ct.getSchemaNames()));
562        }
563
564        // find the schemas
565        List<Schema> docTypeSchemas = new ArrayList<>();
566        for (String schemaName : schemaNames) {
567            Schema schema = schemas.get(schemaName);
568            if (schema == null) {
569                log.error("Document type: " + name + " uses unknown schema: " + schemaName);
570                continue;
571            }
572            docTypeSchemas.add(schema);
573        }
574
575        // create doctype
576        PrefetchInfo prefetch = dtd.prefetch == null ? prefetchInfo : new PrefetchInfo(dtd.prefetch);
577        DocumentTypeImpl docType = new DocumentTypeImpl(name, parent, docTypeSchemas, facetNames, prefetch);
578        registerDocumentType(docType);
579
580        return docType;
581    }
582
583    protected void registerDocumentType(DocumentTypeImpl docType) {
584        String name = docType.getName();
585        documentTypes.put(name, docType);
586        documentTypesExtending.put(name, new HashSet<>(Collections.singleton(name)));
587    }
588
589    @Override
590    public DocumentType getDocumentType(String name) {
591        checkDirty();
592        return documentTypes.get(name);
593    }
594
595    @Override
596    public Set<String> getDocumentTypeNamesForFacet(String facet) {
597        checkDirty();
598        return documentTypesForFacet.get(facet);
599    }
600
601    @Override
602    public Set<String> getDocumentTypeNamesExtending(String docTypeName) {
603        checkDirty();
604        return documentTypesExtending.get(docTypeName);
605    }
606
607    @Override
608    public DocumentType[] getDocumentTypes() {
609        checkDirty();
610        return new ArrayList<DocumentType>(documentTypes.values()).toArray(new DocumentType[0]);
611    }
612
613    @Override
614    public int getDocumentTypesCount() {
615        checkDirty();
616        return documentTypes.size();
617    }
618
619    @Override
620    public boolean hasSuperType(String docType, String superType) {
621        if (docType == null || superType == null) {
622            return false;
623        }
624        Set<String> subTypes = getDocumentTypeNamesExtending(superType);
625        return subTypes != null && subTypes.contains(docType);
626    }
627
628    /*
629     * ===== Proxies =====
630     */
631
632    protected void recomputeProxies() {
633        List<Schema> list = new ArrayList<>();
634        Set<String> nameSet = new HashSet<>();
635        for (ProxiesDescriptor pd : allProxies) {
636            if (!pd.getType().equals("*")) {
637                log.error("Proxy descriptor for specific type not supported: " + pd);
638            }
639            for (String schemaName : pd.getSchemas()) {
640                if (nameSet.contains(schemaName)) {
641                    continue;
642                }
643                Schema schema = schemas.get(schemaName);
644                if (schema == null) {
645                    log.error("Proxy schema uses unknown schema: " + schemaName);
646                    continue;
647                }
648                list.add(schema);
649                nameSet.add(schemaName);
650            }
651        }
652        proxySchemas = list;
653        proxySchemaNames = nameSet;
654    }
655
656    @Override
657    public List<Schema> getProxySchemas(String docType) {
658        // docType unused for now
659        checkDirty();
660        return new ArrayList<>(proxySchemas);
661    }
662
663    @Override
664    public boolean isProxySchema(String schema, String docType) {
665        // docType unused for now
666        checkDirty();
667        return proxySchemaNames.contains(schema);
668    }
669
670    /*
671     * ===== Fields =====
672     */
673
674    @Override
675    public Field getField(String xpath) {
676        checkDirty();
677        Field field = null;
678        if (xpath != null && xpath.contains("/")) {
679            // need to resolve subfields
680            String[] properties = xpath.split("/");
681            Field resolvedField = getField(properties[0]);
682            for (int x = 1; x < properties.length; x++) {
683                if (resolvedField == null) {
684                    break;
685                }
686                resolvedField = getField(resolvedField, properties[x], x == properties.length - 1);
687            }
688            if (resolvedField != null) {
689                field = resolvedField;
690            }
691        } else {
692            field = fields.get(xpath);
693            if (field == null) {
694                QName qname = QName.valueOf(xpath);
695                String prefix = qname.getPrefix();
696                Schema schema = getSchemaFromPrefix(prefix);
697                if (schema == null) {
698                    // try using the name
699                    schema = getSchema(prefix);
700                }
701                if (schema != null) {
702                    field = schema.getField(qname.getLocalName());
703                    if (field != null) {
704                        // map is concurrent so parallelism is ok
705                        fields.put(xpath, field);
706                    }
707                }
708            }
709        }
710        return field;
711    }
712
713    @Override
714    public Field getField(Field parent, String subFieldName) {
715        return getField(parent, subFieldName, true);
716    }
717
718    protected Field getField(Field parent, String subFieldName, boolean finalCall) {
719        if (parent != null) {
720            Type type = parent.getType();
721            if (type.isListType()) {
722                ListType listType = (ListType) type;
723                // remove indexes in case of multiple values
724                if ("*".equals(subFieldName)) {
725                    if (!finalCall) {
726                        return parent;
727                    } else {
728                        return resolveSubField(listType, null, true);
729                    }
730                }
731                try {
732                    Integer.valueOf(subFieldName);
733                    if (!finalCall) {
734                        return parent;
735                    } else {
736                        return resolveSubField(listType, null, true);
737                    }
738                } catch (NumberFormatException e) {
739                    return resolveSubField(listType, subFieldName, false);
740                }
741            } else if (type.isComplexType()) {
742                return ((ComplexType) type).getField(subFieldName);
743            }
744        }
745        return null;
746    }
747
748    protected Field resolveSubField(ListType listType, String subName, boolean fallbackOnSubElement) {
749        Type itemType = listType.getFieldType();
750        if (itemType.isComplexType() && subName != null) {
751            ComplexType complexType = (ComplexType) itemType;
752            Field subField = complexType.getField(subName);
753            return subField;
754        }
755        if (fallbackOnSubElement) {
756            return listType.getField();
757        }
758        return null;
759    }
760
761    public void flushPendingsRegistration() {
762        checkDirty();
763    }
764
765}