001/*
002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019
020package org.nuxeo.ecm.core.storage.sql;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.LinkedHashMap;
029import java.util.LinkedHashSet;
030import java.util.List;
031import java.util.Map;
032import java.util.Map.Entry;
033import java.util.Set;
034
035import org.apache.commons.lang3.StringUtils;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.api.repository.FulltextConfiguration;
040import org.nuxeo.ecm.core.schema.DocumentType;
041import org.nuxeo.ecm.core.schema.PrefetchInfo;
042import org.nuxeo.ecm.core.schema.SchemaManager;
043import org.nuxeo.ecm.core.schema.types.ComplexType;
044import org.nuxeo.ecm.core.schema.types.CompositeType;
045import org.nuxeo.ecm.core.schema.types.Field;
046import org.nuxeo.ecm.core.schema.types.ListType;
047import org.nuxeo.ecm.core.schema.types.Schema;
048import org.nuxeo.ecm.core.schema.types.Type;
049import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
050import org.nuxeo.ecm.core.schema.types.primitives.DateType;
051import org.nuxeo.ecm.core.schema.types.primitives.LongType;
052import org.nuxeo.ecm.core.schema.types.primitives.StringType;
053import org.nuxeo.ecm.core.storage.FulltextConfigurationFactory;
054import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor.FieldDescriptor;
055import org.nuxeo.ecm.core.storage.sql.RowMapper.IdWithTypes;
056import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo;
057import org.nuxeo.runtime.api.Framework;
058
059/**
060 * The {@link Model} is the link between high-level types and SQL-level objects (entity tables, collections). It defines
061 * all policies relating to the choice of structure (what schema are grouped together in for optimization) and names in
062 * the SQL database (table names, column names), and to what entity names (type name, field name) they correspond.
063 * <p>
064 * A Nuxeo schema or type is mapped to a SQL-level table. Several types can be aggregated in the same table. In theory,
065 * a type could even be split into different tables.
066 *
067 * @author Florent Guillaume
068 */
069public class Model {
070
071    private static final Log log = LogFactory.getLog(Model.class);
072
073    public static final String ROOT_TYPE = "Root";
074
075    public static final String REPOINFO_TABLE_NAME = "repositories";
076
077    public static final String REPOINFO_REPONAME_KEY = "name";
078
079    public static final String MAIN_KEY = "id";
080
081    public static final String CLUSTER_NODES_TABLE_NAME = "cluster_nodes";
082
083    public static final String CLUSTER_NODES_NODEID_KEY = "nodeid";
084
085    public static final String CLUSTER_NODES_CREATED_KEY = "created";
086
087    public static final String CLUSTER_INVALS_TABLE_NAME = "cluster_invals";
088
089    public static final String CLUSTER_INVALS_NODEID_KEY = "nodeid";
090
091    public static final String CLUSTER_INVALS_ID_KEY = "id";
092
093    public static final String CLUSTER_INVALS_FRAGMENTS_KEY = "fragments";
094
095    public static final String CLUSTER_INVALS_KIND_KEY = "kind";
096
097    public static final String MAIN_PRIMARY_TYPE_PROP = "ecm:primaryType";
098
099    public static final String MAIN_PRIMARY_TYPE_KEY = "primarytype";
100
101    public static final String MAIN_MIXIN_TYPES_PROP = "ecm:mixinTypes";
102
103    public static final String MAIN_MIXIN_TYPES_KEY = "mixintypes";
104
105    public static final String MAIN_IS_RETENTION_ACTIVE_PROP = "ecm:isRetentionActive";
106
107    public static final String MAIN_IS_RETENTION_ACTIVE_KEY = "isretentionactive";
108
109    public static final String MAIN_BASE_VERSION_PROP = "ecm:baseVersion";
110
111    public static final String MAIN_BASE_VERSION_KEY = "baseversionid";
112
113    public static final String MAIN_CHECKED_IN_PROP = "ecm:isCheckedIn";
114
115    public static final String MAIN_CHECKED_IN_KEY = "ischeckedin";
116
117    public static final String MAIN_MAJOR_VERSION_PROP = "ecm:majorVersion";
118
119    public static final String MAIN_MAJOR_VERSION_KEY = "majorversion";
120
121    public static final String MAIN_MINOR_VERSION_PROP = "ecm:minorVersion";
122
123    public static final String MAIN_MINOR_VERSION_KEY = "minorversion";
124
125    public static final String MAIN_IS_VERSION_PROP = "ecm:isVersion";
126
127    public static final String MAIN_IS_VERSION_KEY = "isversion";
128
129    public static final String MAIN_SYS_CHANGE_TOKEN_PROP = "ecm:systemChangeToken";
130
131    public static final String MAIN_SYS_CHANGE_TOKEN_KEY = "systemchangetoken";
132
133    public static final String MAIN_CHANGE_TOKEN_PROP = "ecm:changeToken";
134
135    public static final String MAIN_CHANGE_TOKEN_KEY = "changetoken";
136
137    // for soft-delete
138    public static final String MAIN_IS_DELETED_PROP = "ecm:isDeleted";
139
140    // for soft-delete
141    public static final String MAIN_IS_DELETED_KEY = "isdeleted";
142
143    // for soft-delete
144    public static final String MAIN_DELETED_TIME_PROP = "ecm:deletedTime";
145
146    // for soft-delete
147    public static final String MAIN_DELETED_TIME_KEY = "deletedtime";
148
149    /**
150     * @since 10.1
151     */
152    public static final String MAIN_IS_TRASHED_PROP = "ecm:isTrashed";
153
154    /**
155     * @since 10.1
156     */
157    public static final String MAIN_IS_TRASHED_KEY = "istrashed";
158
159    public static final String UID_SCHEMA_NAME = "uid";
160
161    public static final String UID_MAJOR_VERSION_KEY = "major_version";
162
163    public static final String UID_MINOR_VERSION_KEY = "minor_version";
164
165    public static final String HIER_TABLE_NAME = "hierarchy";
166
167    public static final String HIER_PARENT_KEY = "parentid";
168
169    public static final String HIER_CHILD_NAME_KEY = "name";
170
171    public static final String HIER_CHILD_POS_KEY = "pos";
172
173    public static final String HIER_CHILD_ISPROPERTY_KEY = "isproperty";
174
175    public static final String ANCESTORS_TABLE_NAME = "ancestors";
176
177    public static final String ANCESTORS_ANCESTOR_KEY = "ancestors";
178
179    public static final String COLL_TABLE_POS_KEY = "pos";
180
181    public static final String COLL_TABLE_VALUE_KEY = "item";
182
183    public static final String MISC_TABLE_NAME = "misc";
184
185    public static final String MISC_LIFECYCLE_POLICY_PROP = "ecm:lifeCyclePolicy";
186
187    public static final String MISC_LIFECYCLE_POLICY_KEY = "lifecyclepolicy";
188
189    public static final String MISC_LIFECYCLE_STATE_PROP = "ecm:lifeCycleState";
190
191    public static final String MISC_LIFECYCLE_STATE_KEY = "lifecyclestate";
192
193    public static final String ACL_TABLE_NAME = "acls";
194
195    public static final String ACL_PROP = "ecm:acl";
196
197    public static final String ACL_POS_KEY = "pos";
198
199    public static final String ACL_NAME_KEY = "name";
200
201    public static final String ACL_GRANT_KEY = "grant";
202
203    public static final String ACL_PERMISSION_KEY = "permission";
204
205    public static final String ACL_CREATOR_KEY = "creator";
206
207    public static final String ACL_BEGIN_KEY = "begin";
208
209    public static final String ACL_END_KEY = "end";
210
211    public static final String ACL_STATUS_KEY = "status";
212
213    public static final String ACL_USER_KEY = "user";
214
215    public static final String ACL_GROUP_KEY = "group";
216
217    public static final String VERSION_TABLE_NAME = "versions";
218
219    public static final String VERSION_VERSIONABLE_PROP = "ecm:versionableId";
220
221    public static final String VERSION_VERSIONABLE_KEY = "versionableid";
222
223    public static final String VERSION_CREATED_PROP = "ecm:versionCreated";
224
225    public static final String VERSION_CREATED_KEY = "created";
226
227    public static final String VERSION_LABEL_PROP = "ecm:versionLabel";
228
229    public static final String VERSION_LABEL_KEY = "label";
230
231    public static final String VERSION_DESCRIPTION_PROP = "ecm:versionDescription";
232
233    public static final String VERSION_DESCRIPTION_KEY = "description";
234
235    public static final String VERSION_IS_LATEST_PROP = "ecm:isLatestVersion";
236
237    public static final String VERSION_IS_LATEST_KEY = "islatest";
238
239    public static final String VERSION_IS_LATEST_MAJOR_PROP = "ecm:isLatestMajorVersion";
240
241    public static final String VERSION_IS_LATEST_MAJOR_KEY = "islatestmajor";
242
243    public static final String PROXY_TYPE = "ecm:proxy";
244
245    public static final String PROXY_TABLE_NAME = "proxies";
246
247    public static final String PROXY_TARGET_PROP = "ecm:proxyTargetId";
248
249    public static final String PROXY_TARGET_KEY = "targetid";
250
251    public static final String PROXY_VERSIONABLE_PROP = "ecm:proxyVersionableId";
252
253    public static final String PROXY_VERSIONABLE_KEY = "versionableid";
254
255    public static final String LOCK_TABLE_NAME = "locks";
256
257    public static final String LOCK_OWNER_PROP = "ecm:lockOwner";
258
259    public static final String LOCK_OWNER_KEY = "owner";
260
261    public static final String LOCK_CREATED_PROP = "ecm:lockCreated";
262
263    public static final String LOCK_CREATED_KEY = "created";
264
265    public static final String FULLTEXT_DEFAULT_INDEX = "default"; // not
266                                                                   // config
267
268    public static final String FULLTEXT_TABLE_NAME = "fulltext";
269
270    public static final String FULLTEXT_JOBID_PROP = "ecm:fulltextJobId";
271
272    public static final String FULLTEXT_JOBID_KEY = "jobid";
273
274    public static final String FULLTEXT_FULLTEXT_PROP = "ecm:fulltext";
275
276    public static final String FULLTEXT_FULLTEXT_KEY = "fulltext";
277
278    public static final String FULLTEXT_SIMPLETEXT_PROP = "ecm:simpleText";
279
280    public static final String FULLTEXT_SIMPLETEXT_KEY = "simpletext";
281
282    public static final String FULLTEXT_BINARYTEXT_PROP = "ecm:binaryText";
283
284    public static final String FULLTEXT_BINARYTEXT_KEY = "binarytext";
285
286    public static final String HIER_READ_ACL_TABLE_NAME = "hierarchy_read_acl";
287
288    public static final String HIER_READ_ACL_ID = "id";
289
290    public static final String HIER_READ_ACL_ACL_ID = "acl_id";
291
292    public static final String ACLR_USER_MAP_TABLE_NAME = "aclr_user_map";
293
294    public static final String ACLR_USER_MAP_USER_ID = "user_id";
295
296    public static final String ACLR_USER_MAP_ACL_ID = "acl_id";
297
298    /** Specified in ext. point to use CLOBs. */
299    public static final String FIELD_TYPE_LARGETEXT = "largetext";
300
301    /** Specified in ext. point to use array instead of collection table. */
302    public static final String FIELD_TYPE_ARRAY = "array";
303
304    /** Specified in ext. point to use CLOB array instead of collection table. */
305    public static final String FIELD_TYPE_ARRAY_LARGETEXT = "array_largetext";
306
307    // some random long that's not in the database
308    // first half of md5 of "nosuchlongid"
309    public static final Long NO_SUCH_LONG_ID = Long.valueOf(0x3153147dd69fcea4L);
310
311    public static final Long INITIAL_CHANGE_TOKEN = Long.valueOf(0);
312
313    public static final Long INITIAL_SYS_CHANGE_TOKEN = Long.valueOf(0);
314
315    protected final boolean softDeleteEnabled;
316
317    protected final boolean proxiesEnabled;
318
319    protected final boolean changeTokenEnabled;
320
321    /** Type of ids as seen by the VCS Java layer. */
322    public enum IdType {
323        STRING, //
324        LONG, //
325    }
326
327    // type of id seen by the VCS Java layer
328    protected final IdType idType;
329
330    // type for VCS row storage
331    protected final PropertyType idPropertyType;
332
333    // type for core properties
334    protected final Type idCoreType;
335
336    /**
337     * If true, the misc columns are added to hierarchy, not to a separate misc table.
338     */
339    protected final boolean miscInHierarchy;
340
341    protected final RepositoryDescriptor repositoryDescriptor;
342
343    /** Per-doctype list of schemas. */
344    private final Map<String, Set<String>> allDocTypeSchemas;
345
346    /** Per-mixin list of schemas. */
347    private final Map<String, Set<String>> allMixinSchemas;
348
349    /** The proxy schemas. */
350    private final Set<String> allProxySchemas;
351
352    /** Map of mixin to doctypes. */
353    private final Map<String, Set<String>> mixinsDocumentTypes;
354
355    /** Map of doctype to mixins, for search. */
356    protected final Map<String, Set<String>> documentTypesMixins;
357
358    /** Shared high-level properties that don't come from the schema manager. */
359    private final Map<String, Type> specialPropertyTypes;
360
361    /** Map of fragment to key to property info. */
362    private final Map<String, Map<String, ModelProperty>> fragmentPropertyInfos;
363
364    /** Map of schema to property to property info. */
365    private final Map<String, Map<String, ModelProperty>> schemaPropertyInfos;
366
367    /** Map of docType/complexType to property to property info. */
368    private final Map<String, Map<String, ModelProperty>> typePropertyInfos;
369
370    /** Map of mixin to property to property info. */
371    private final Map<String, Map<String, ModelProperty>> mixinPropertyInfos;
372
373    /** The proxy property infos. */
374    private final Map<String, ModelProperty> proxyPropertyInfos;
375
376    /** Map of property to property info. */
377    private final Map<String, ModelProperty> sharedPropertyInfos;
378
379    /** Merged properties (all schemas together + shared). */
380    private final Map<String, ModelProperty> mergedPropertyInfos;
381
382    /** Per-schema map of path to property info. */
383    private final Map<String, Map<String, ModelProperty>> schemaPathPropertyInfos;
384
385    /** Map of prefix to schema. */
386    private final Map<String, String> prefixToSchema;
387
388    /** Per-schema set of path to simple fulltext properties. */
389    private final Map<String, Set<String>> schemaSimpleTextPaths;
390
391    /**
392     * Map of path (from all doc types) to property info. Value is NONE for valid complex property path prefixes.
393     */
394    private final Map<String, ModelProperty> allPathPropertyInfos;
395
396    /** Map of fragment to key to column type. */
397    private final Map<String, Map<String, ColumnType>> fragmentKeyTypes;
398
399    /** Map of fragment to keys for binary columns. */
400    private final Map<String, List<String>> binaryFragmentKeys;
401
402    /** Maps collection table names to their type. */
403    private final Map<String, PropertyType> collectionTables;
404
405    /** Column ordering for collections. */
406    private final Map<String, String> collectionOrderBy;
407
408    // -------------------------------------------------------
409
410    /**
411     * Map of schema to simple+collection fragments. Used while computing document type fragments, and for prefetch.
412     */
413    private final Map<String, Set<String>> schemaFragments;
414
415    /** Map of doctype/complextype to simple+collection fragments. */
416    protected final Map<String, Set<String>> typeFragments;
417
418    /** Map of mixin to simple+collection fragments. */
419    protected final Map<String, Set<String>> mixinFragments;
420
421    /** The proxy fragments. */
422    private final Set<String> proxyFragments;
423
424    /** Map of doctype to prefetched fragments. */
425    protected final Map<String, Set<String>> docTypePrefetchedFragments;
426
427    /** Map of schema to child name to type. */
428    protected final Map<String, Map<String, String>> schemaComplexChildren;
429
430    /** Map of doctype/complextype to child name to type. */
431    protected final Map<String, Map<String, String>> typeComplexChildren;
432
433    /** Map of mixin to child name to type. */
434    protected final Map<String, Map<String, String>> mixinComplexChildren;
435
436    /** Map of doctype to its supertype, for search. */
437    protected final Map<String, String> documentSuperTypes;
438
439    /** Map of doctype to its subtypes (including itself), for search. */
440    protected final Map<String, Set<String>> documentSubTypes;
441
442    /** Map of field name to fragment holding it. Used for prefetch. */
443    protected final Map<String, String> fieldFragment;
444
445    protected final FulltextConfiguration fulltextConfiguration;
446
447    protected final Set<String> noPerDocumentQueryFacets;
448
449    /**
450     * Map of fragment -> info about whether there's a fulltext text field (PropertyType.STRING), binary field
451     * (PropertyType.BINARY), or both (PropertyType.BOOLEAN).
452     */
453    protected final Map<String, PropertyType> fulltextInfoByFragment;
454
455    private final boolean materializeFulltextSyntheticColumn;
456
457    private final boolean supportsArrayColumns;
458
459    public Model(ModelSetup modelSetup) {
460        repositoryDescriptor = modelSetup.repositoryDescriptor;
461        materializeFulltextSyntheticColumn = modelSetup.materializeFulltextSyntheticColumn;
462        supportsArrayColumns = modelSetup.supportsArrayColumns;
463        idType = modelSetup.idType;
464        switch (idType) {
465        case STRING:
466            idPropertyType = PropertyType.STRING;
467            idCoreType = StringType.INSTANCE;
468            break;
469        case LONG:
470            idPropertyType = PropertyType.LONG;
471            idCoreType = LongType.INSTANCE;
472            break;
473        default:
474            throw new AssertionError(idType.toString());
475        }
476        softDeleteEnabled = repositoryDescriptor.getSoftDeleteEnabled();
477        proxiesEnabled = repositoryDescriptor.getProxiesEnabled();
478        changeTokenEnabled = repositoryDescriptor.isChangeTokenEnabled();
479
480        allDocTypeSchemas = new HashMap<>();
481        mixinsDocumentTypes = new HashMap<>();
482        documentTypesMixins = new HashMap<>();
483        allMixinSchemas = new HashMap<>();
484        allProxySchemas = new HashSet<>();
485
486        fragmentPropertyInfos = new HashMap<>();
487        schemaPropertyInfos = new HashMap<>();
488        typePropertyInfos = new HashMap<>();
489        mixinPropertyInfos = new HashMap<>();
490        proxyPropertyInfos = new HashMap<>();
491        sharedPropertyInfos = new HashMap<>();
492        mergedPropertyInfos = new HashMap<>();
493        schemaPathPropertyInfos = new HashMap<>();
494        prefixToSchema = new HashMap<>();
495        schemaSimpleTextPaths = new HashMap<>();
496        allPathPropertyInfos = new HashMap<>();
497        fulltextInfoByFragment = new HashMap<>();
498        fragmentKeyTypes = new HashMap<>();
499        binaryFragmentKeys = new HashMap<>();
500
501        collectionTables = new HashMap<>();
502        collectionOrderBy = new HashMap<>();
503
504        schemaFragments = new HashMap<>();
505        typeFragments = new HashMap<>();
506        mixinFragments = new HashMap<>();
507        proxyFragments = new HashSet<>();
508        docTypePrefetchedFragments = new HashMap<>();
509        fieldFragment = new HashMap<>();
510
511        schemaComplexChildren = new HashMap<>();
512        typeComplexChildren = new HashMap<>();
513        mixinComplexChildren = new HashMap<>();
514
515        documentSuperTypes = new HashMap<>();
516        documentSubTypes = new HashMap<>();
517
518        specialPropertyTypes = new HashMap<>();
519        noPerDocumentQueryFacets = new HashSet<>();
520
521        miscInHierarchy = false;
522
523        if (repositoryDescriptor.getFulltextDescriptor().getFulltextDisabled()) {
524            fulltextConfiguration = null;
525        } else {
526            fulltextConfiguration = FulltextConfigurationFactory.make(repositoryDescriptor.getFulltextDescriptor());
527        }
528
529        initMainModel();
530        initVersionsModel();
531        if (proxiesEnabled) {
532            initProxiesModel();
533        }
534        initLocksModel();
535        initAclModel();
536        initMiscModel();
537        // models for all document types and mixins
538        initModels();
539        if (fulltextConfiguration != null) {
540            inferFulltextInfoByFragment(); // needs mixin schemas
541            initFullTextModel();
542        }
543    }
544
545    /**
546     * Gets the repository descriptor used for this model.
547     *
548     * @return the repository descriptor
549     */
550    public RepositoryDescriptor getRepositoryDescriptor() {
551        return repositoryDescriptor;
552    }
553
554    /**
555     * Fixup an id that has been turned into a string for high-level Nuxeo APIs.
556     *
557     * @param id the id to fixup
558     * @return the fixed up id
559     */
560    public Serializable idFromString(String id) {
561        switch (idType) {
562        case STRING:
563            return id;
564        case LONG:
565            // use isNumeric instead of try/catch for efficiency
566            if (StringUtils.isNumeric(id)) {
567                return Long.valueOf(id);
568            } else {
569                return NO_SUCH_LONG_ID;
570            }
571        default:
572            throw new AssertionError(idType.toString());
573        }
574    }
575
576    /**
577     * Turns an id that may be a String or a Long into a String for high-level Nuxeo APIs.
578     *
579     * @param id the serializable id
580     * @return the string
581     */
582    public String idToString(Serializable id) {
583        return id.toString();
584    }
585
586    /**
587     * Records info about a system property (applying to all document types).
588     */
589    private void addPropertyInfo(String propertyName, PropertyType propertyType, String fragmentName,
590            String fragmentKey, boolean readonly, Type coreType, ColumnType type) {
591        addPropertyInfo(null, false, propertyName, propertyType, fragmentName, fragmentKey, readonly, coreType, type);
592    }
593
594    /**
595     * Records info about one property (for a given type). Used for proxy properties.
596     */
597    private void addPropertyInfo(String typeName, String propertyName, PropertyType propertyType, String fragmentName,
598            String fragmentKey, boolean readonly, Type coreType, ColumnType type) {
599        addPropertyInfo(typeName, false, propertyName, propertyType, fragmentName, fragmentKey, readonly, coreType,
600                type);
601    }
602
603    /**
604     * Records info about one property from a complex type or schema.
605     */
606    private void addPropertyInfo(ComplexType complexType, String propertyName, PropertyType propertyType,
607            String fragmentName, String fragmentKey, boolean readonly, Type coreType, ColumnType type) {
608        String typeName = complexType.getName();
609        boolean isSchema = complexType instanceof Schema;
610        addPropertyInfo(typeName, isSchema, propertyName, propertyType, fragmentName, fragmentKey, readonly, coreType,
611                type);
612    }
613
614    /**
615     * Records info about one property, in a schema-based structure and in a fragment-based structure.
616     */
617    private void addPropertyInfo(String typeName, boolean isSchema, String propertyName, PropertyType propertyType,
618            String fragmentName, String fragmentKey, boolean readonly, Type coreType, ColumnType type) {
619
620        ModelProperty propertyInfo = new ModelProperty(propertyType, fragmentName, fragmentKey, readonly);
621
622        // per type/schema property info
623        Map<String, ModelProperty> propertyInfos;
624        if (typeName == null) {
625            propertyInfos = sharedPropertyInfos;
626        } else {
627            Map<String, Map<String, ModelProperty>> map = isSchema ? schemaPropertyInfos : typePropertyInfos;
628            propertyInfos = map.computeIfAbsent(typeName, k -> new HashMap<>());
629        }
630        propertyInfos.put(propertyName, propertyInfo);
631
632        // merged properties
633        ModelProperty previous = mergedPropertyInfos.get(propertyName);
634        if (previous == null) {
635            mergedPropertyInfos.put(propertyName, propertyInfo);
636        } else {
637            log.debug(String.format(
638                    "Schemas '%s' and '%s' both have a property '%s', "
639                            + "unqualified reference in queries will use schema '%1$s'",
640                    previous.fragmentName, fragmentName, propertyName));
641        }
642        // compatibility for use of schema name as prefix
643        if (typeName != null && !propertyName.contains(":")) {
644            // allow schema name as prefix
645            propertyName = typeName + ':' + propertyName;
646            mergedPropertyInfos.putIfAbsent(propertyName, propertyInfo);
647        }
648
649        // system properties core type (no core schema available)
650        if (coreType != null) {
651            specialPropertyTypes.put(propertyName, coreType);
652        }
653
654        // per-fragment property info
655        Map<String, ModelProperty> fragmentKeyInfos = fragmentPropertyInfos.computeIfAbsent(fragmentName,
656                k -> new HashMap<>());
657        if (fragmentKey != null) {
658            fragmentKeyInfos.put(fragmentKey, propertyInfo);
659        }
660
661        // per-fragment keys type
662        if (fragmentKey != null && type != null) {
663            fragmentKeyTypes.computeIfAbsent(fragmentName, k -> new LinkedHashMap<>()).put(fragmentKey, type);
664
665            // record binary columns for the GC
666            if (type.spec == ColumnSpec.BLOBID) {
667                binaryFragmentKeys.computeIfAbsent(fragmentName, k -> new ArrayList<>(1)).add(fragmentKey);
668            }
669        } // else collection, uses addCollectionFragmentInfos directly
670    }
671
672    private void addCollectionFragmentInfos(String fragmentName, PropertyType propertyType, String orderBy,
673            Map<String, ColumnType> keysType) {
674        collectionTables.put(fragmentName, propertyType);
675        collectionOrderBy.put(fragmentName, orderBy);
676        fragmentKeyTypes.put(fragmentName, keysType);
677    }
678
679    // fill the map for key with property infos for the given schemas
680    // different maps are used for doctypes or mixins
681    private void inferPropertyInfos(Map<String, Map<String, ModelProperty>> map, String key, Set<String> schemaNames) {
682        Map<String, ModelProperty> propertyInfos = map.computeIfAbsent(key, k -> new HashMap<>());
683        for (String schemaName : schemaNames) {
684            Map<String, ModelProperty> infos = schemaPropertyInfos.get(schemaName);
685            if (infos != null) {
686                propertyInfos.putAll(infos);
687            }
688            // else schema with no properties (complex list)
689        }
690    }
691
692    /**
693     * Infers all possible paths for properties in a schema.
694     */
695    private void inferSchemaPropertyPaths(Schema schema) {
696        String schemaName = schema.getName();
697        if (schemaPathPropertyInfos.containsKey(schemaName)) {
698            return;
699        }
700        Map<String, ModelProperty> propertyInfoByPath = new HashMap<>();
701        inferTypePropertyPaths(schema, "", propertyInfoByPath, null);
702        schemaPathPropertyInfos.put(schemaName, propertyInfoByPath);
703        // allow schema-as-prefix if schemas has no prefix, if non-complex
704        Map<String, ModelProperty> alsoWithPrefixes = new HashMap<>(propertyInfoByPath);
705        String prefix = schema.getNamespace().prefix;
706        if (prefix.isEmpty()) {
707            for (Entry<String, ModelProperty> e : propertyInfoByPath.entrySet()) {
708                alsoWithPrefixes.put(schemaName + ':' + e.getKey(), e.getValue());
709            }
710        } else {
711            prefixToSchema.put(prefix, schemaName);
712        }
713        allPathPropertyInfos.putAll(alsoWithPrefixes);
714        // those for simpletext properties
715        Set<String> simplePaths = new HashSet<>();
716        for (Entry<String, ModelProperty> entry : propertyInfoByPath.entrySet()) {
717            ModelProperty pi = entry.getValue();
718            if (pi.isIntermediateSegment()) {
719                continue;
720            }
721            if (pi.propertyType != PropertyType.STRING && pi.propertyType != PropertyType.ARRAY_STRING) {
722                continue;
723            }
724            simplePaths.add(entry.getKey());
725        }
726        schemaSimpleTextPaths.put(schemaName, simplePaths);
727    }
728
729    // recurses in a schema or complex type
730    private void inferTypePropertyPaths(ComplexType complexType, String prefix,
731            Map<String, ModelProperty> propertyInfoByPath, Set<String> done) {
732        if (done == null) {
733            done = new LinkedHashSet<>();
734        }
735        String typeName = complexType.getName();
736        if (done.contains(typeName)) {
737            log.warn("Complex type " + typeName + " refers to itself recursively: " + done);
738            // stop recursion
739            return;
740        }
741        done.add(typeName);
742
743        for (Field field : complexType.getFields()) {
744            String propertyName = field.getName().getPrefixedName();
745            String path = prefix + propertyName;
746            Type fieldType = field.getType();
747            if (fieldType.isComplexType()) {
748                // complex type
749                propertyInfoByPath.put(path, new ModelProperty(propertyName));
750                inferTypePropertyPaths((ComplexType) fieldType, path + '/', propertyInfoByPath, done);
751                continue;
752            } else if (fieldType.isListType()) {
753                Type listFieldType = ((ListType) fieldType).getFieldType();
754                if (!listFieldType.isSimpleType()) {
755                    // complex list
756                    propertyInfoByPath.put(path + "/*", new ModelProperty(propertyName));
757                    inferTypePropertyPaths((ComplexType) listFieldType, path + "/*/", propertyInfoByPath, done);
758                    continue;
759                }
760                // else array
761            }
762            // else primitive type
763            // in both cases, record it
764            Map<String, Map<String, ModelProperty>> map = (complexType instanceof Schema) ? schemaPropertyInfos
765                    : typePropertyInfos;
766            ModelProperty pi = map.get(typeName).get(propertyName);
767            propertyInfoByPath.put(path, pi);
768            // also add the propname/* path for array elements
769            if (pi.propertyType.isArray()) {
770                propertyInfoByPath.put(path + "/*", pi);
771                if (!supportsArrayColumns) {
772                    // pseudo-syntax with ending "#" to get to the pos column
773                    String posPropertyName = propertyName + "#";
774                    ModelProperty posPi = map.get(typeName).get(posPropertyName);
775                    propertyInfoByPath.put(path + "#", posPi);
776                }
777            }
778        }
779        done.remove(typeName);
780    }
781
782    private void inferFulltextInfoByFragment() {
783        // simple fragments
784        for (Entry<String, Map<String, ModelProperty>> es : fragmentPropertyInfos.entrySet()) {
785            String fragmentName = es.getKey();
786            Map<String, ModelProperty> infos = es.getValue();
787            if (infos == null) {
788                continue;
789            }
790            PropertyType type = null;
791            for (ModelProperty info : infos.values()) {
792                if (info != null && info.fulltext) {
793                    PropertyType t = info.propertyType;
794                    if (t == PropertyType.STRING || t == PropertyType.BINARY) {
795                        if (type == null) {
796                            type = t;
797                            continue;
798                        }
799                        if (type != t) {
800                            type = PropertyType.BOOLEAN; // both
801                            break;
802                        }
803                    }
804                }
805            }
806            fulltextInfoByFragment.put(fragmentName, type);
807        }
808        // collection fragments
809        for (Entry<String, PropertyType> es : collectionTables.entrySet()) {
810            String fragmentName = es.getKey();
811            PropertyType type = es.getValue();
812            if (type == PropertyType.ARRAY_STRING || type == PropertyType.ARRAY_BINARY) {
813                fulltextInfoByFragment.put(fragmentName, type.getArrayBaseType());
814            }
815        }
816    }
817
818    /** Get doctype/complextype property info. */
819    public ModelProperty getPropertyInfo(String typeName, String propertyName) {
820        Map<String, ModelProperty> propertyInfos = typePropertyInfos.get(typeName);
821        if (propertyInfos == null) {
822            // no such doctype/complextype
823            return null;
824        }
825        ModelProperty propertyInfo = propertyInfos.get(propertyName);
826        return propertyInfo != null ? propertyInfo : sharedPropertyInfos.get(propertyName);
827    }
828
829    public Map<String, ModelProperty> getMixinPropertyInfos(String mixin) {
830        return mixinPropertyInfos.get(mixin);
831    }
832
833    // for all types for now
834    public ModelProperty getProxySchemasPropertyInfo(String propertyName) {
835        ModelProperty propertyInfo = proxyPropertyInfos.get(propertyName);
836        return propertyInfo != null ? propertyInfo : sharedPropertyInfos.get(propertyName);
837    }
838
839    public ModelProperty getMixinPropertyInfo(String mixin, String propertyName) {
840        Map<String, ModelProperty> propertyInfos = mixinPropertyInfos.get(mixin);
841        if (propertyInfos == null) {
842            // no such mixin
843            return null;
844        }
845        return propertyInfos.get(propertyName);
846    }
847
848    public ModelProperty getPropertyInfo(String propertyName) {
849        return mergedPropertyInfos.get(propertyName);
850    }
851
852    /**
853     * Gets the model of the property for the given path. Returns something with
854     * {@link ModelProperty#isIntermediateSegment()} = true for an intermediate segment of a complex property.
855     */
856    public ModelProperty getPathPropertyInfo(String xpath) {
857        return allPathPropertyInfos.get(xpath);
858    }
859
860    public Set<String> getPropertyInfoNames() {
861        return mergedPropertyInfos.keySet();
862    }
863
864    public ModelProperty getPathPropertyInfo(String primaryType, String[] mixinTypes, String path) {
865        for (String schema : getAllSchemas(primaryType, mixinTypes)) {
866            Map<String, ModelProperty> propertyInfoByPath = schemaPathPropertyInfos.get(schema);
867            if (propertyInfoByPath != null) {
868                ModelProperty pi = propertyInfoByPath.get(path);
869                if (pi != null) {
870                    return pi;
871                }
872            }
873        }
874        return null;
875    }
876
877    public Map<String, String> getTypeComplexChildren(String typeName) {
878        return typeComplexChildren.get(typeName);
879    }
880
881    public Map<String, String> getMixinComplexChildren(String mixin) {
882        return mixinComplexChildren.get(mixin);
883    }
884
885    public Set<String> getSimpleTextPropertyPaths(String primaryType, String[] mixinTypes) {
886        Set<String> paths = new HashSet<>();
887        for (String schema : getAllSchemas(primaryType, mixinTypes)) {
888            Set<String> p = schemaSimpleTextPaths.get(schema);
889            if (p != null) {
890                paths.addAll(p);
891            }
892        }
893        return paths;
894    }
895
896    /**
897     * Checks if the given xpath, when resolved on a proxy, points to a proxy-specific schema instead of the target
898     * document.
899     */
900    public boolean isProxySchemaPath(String xpath) {
901        int p = xpath.indexOf(':');
902        if (p == -1) {
903            return false; // no schema/prefix -> not on proxy
904        }
905        String prefix = xpath.substring(0, p);
906        String schema = prefixToSchema.get(prefix);
907        if (schema == null) {
908            schema = prefix;
909        }
910        return allProxySchemas.contains(schema);
911    }
912
913    private Set<String> getAllSchemas(String primaryType, String[] mixinTypes) {
914        Set<String> schemas = new LinkedHashSet<>();
915        Set<String> s = allDocTypeSchemas.get(primaryType);
916        if (s != null) {
917            schemas.addAll(s);
918        }
919        for (String mixin : mixinTypes) {
920            s = allMixinSchemas.get(mixin);
921            if (s != null) {
922                schemas.addAll(s);
923            }
924        }
925        return schemas;
926    }
927
928    public FulltextConfiguration getFulltextConfiguration() {
929        return fulltextConfiguration;
930    }
931
932    /**
933     * Finds out if a field is to be indexed as fulltext.
934     *
935     * @param fragmentKey the key or {@code null} for a collection
936     * @return {@link PropertyType#STRING} or {@link PropertyType#BINARY} if this field is to be indexed as fulltext
937     */
938    public PropertyType getFulltextFieldType(String fragmentName, String fragmentKey) {
939        if (fragmentKey == null) {
940            PropertyType type = collectionTables.get(fragmentName);
941            if (type == PropertyType.ARRAY_STRING || type == PropertyType.ARRAY_BINARY) {
942                return type.getArrayBaseType();
943            }
944            return null;
945        } else {
946            Map<String, ModelProperty> infos = fragmentPropertyInfos.get(fragmentName);
947            if (infos == null) {
948                return null;
949            }
950            ModelProperty info = infos.get(fragmentKey);
951            if (info != null && info.fulltext) {
952                return info.propertyType;
953            }
954            return null;
955        }
956    }
957
958    /**
959     * Checks if a fragment has any field indexable as fulltext.
960     *
961     * @return PropertyType.STRING, PropertyType.BINARY, or PropertyType.BOOLEAN for both.
962     */
963    public PropertyType getFulltextInfoForFragment(String fragmentName) {
964        return fulltextInfoByFragment.get(fragmentName);
965    }
966
967    public Type getSpecialPropertyType(String propertyName) {
968        return specialPropertyTypes.get(propertyName);
969    }
970
971    public PropertyType getCollectionFragmentType(String fragmentName) {
972        return collectionTables.get(fragmentName);
973    }
974
975    public boolean isCollectionFragment(String fragmentName) {
976        return collectionTables.containsKey(fragmentName);
977    }
978
979    public String getCollectionOrderBy(String fragmentName) {
980        return collectionOrderBy.get(fragmentName);
981    }
982
983    public Set<String> getFragmentNames() {
984        return fragmentKeyTypes.keySet();
985    }
986
987    public Map<String, ColumnType> getFragmentKeysType(String fragmentName) {
988        return fragmentKeyTypes.get(fragmentName);
989    }
990
991    public Map<String, List<String>> getBinaryPropertyInfos() {
992        return binaryFragmentKeys;
993    }
994
995    private void addTypeFragments(String typeName, Set<String> fragmentNames) {
996        typeFragments.put(typeName, fragmentNames);
997    }
998
999    private void addFieldFragment(Field field, String fragmentName) {
1000        String fieldName = field.getName().toString();
1001        fieldFragment.put(fieldName, fragmentName);
1002    }
1003
1004    private void addDocTypePrefetchedFragments(String docTypeName, Set<String> fragmentNames) {
1005        docTypePrefetchedFragments.computeIfAbsent(docTypeName, k -> new HashSet<>()).addAll(fragmentNames);
1006    }
1007
1008    /**
1009     * Gets the fragments for a type (doctype or complex type).
1010     */
1011    private Set<String> getTypeFragments(String docTypeName) {
1012        return typeFragments.get(docTypeName);
1013    }
1014
1015    /**
1016     * Gets the fragments for a mixin.
1017     */
1018    private Set<String> getMixinFragments(String mixin) {
1019        return mixinFragments.get(mixin);
1020    }
1021
1022    public Set<String> getTypePrefetchedFragments(String typeName) {
1023        return docTypePrefetchedFragments.get(typeName);
1024    }
1025
1026    /**
1027     * Checks if we have a type (doctype or complex type).
1028     */
1029    public boolean isType(String typeName) {
1030        return typeFragments.containsKey(typeName);
1031    }
1032
1033    public String getDocumentSuperType(String typeName) {
1034        return documentSuperTypes.get(typeName);
1035    }
1036
1037    public Set<String> getDocumentSubTypes(String typeName) {
1038        return documentSubTypes.get(typeName);
1039    }
1040
1041    public Set<String> getDocumentTypes() {
1042        return documentTypesMixins.keySet();
1043    }
1044
1045    public Set<String> getDocumentTypeFacets(String typeName) {
1046        Set<String> facets = documentTypesMixins.get(typeName);
1047        return facets == null ? Collections.<String> emptySet() : facets;
1048    }
1049
1050    public Set<String> getMixinDocumentTypes(String mixin) {
1051        Set<String> types = mixinsDocumentTypes.get(mixin);
1052        return types == null ? Collections.<String> emptySet() : types;
1053    }
1054
1055    /**
1056     * Given a map of id to types, returns a map of fragment names to ids.
1057     */
1058    public Map<String, Set<Serializable>> getPerFragmentIds(Map<Serializable, IdWithTypes> idToTypes) {
1059        Map<String, Set<Serializable>> allFragmentIds = new HashMap<>();
1060        for (Entry<Serializable, IdWithTypes> e : idToTypes.entrySet()) {
1061            Serializable id = e.getKey();
1062            IdWithTypes typeInfo = e.getValue();
1063            for (String fragmentName : getTypeFragments(typeInfo)) {
1064                allFragmentIds.computeIfAbsent(fragmentName, k -> new HashSet<>()).add(id);
1065            }
1066        }
1067        return allFragmentIds;
1068    }
1069
1070    /**
1071     * Gets the type fragments for a primary type and mixin types. Hierarchy is included.
1072     */
1073    public Set<String> getTypeFragments(IdWithTypes typeInfo) {
1074        Set<String> fragmentNames = new HashSet<>();
1075        fragmentNames.add(HIER_TABLE_NAME);
1076        Set<String> tf = getTypeFragments(typeInfo.primaryType);
1077        if (tf != null) {
1078            // null if unknown type left in the database
1079            fragmentNames.addAll(tf);
1080        }
1081        String[] mixins = typeInfo.mixinTypes;
1082        if (mixins != null) {
1083            for (String mixin : mixins) {
1084                Set<String> mf = getMixinFragments(mixin);
1085                if (mf != null) {
1086                    fragmentNames.addAll(mf);
1087                }
1088            }
1089        }
1090        if (PROXY_TYPE.equals(typeInfo.primaryType)) {
1091            fragmentNames.addAll(proxyFragments);
1092        }
1093        return fragmentNames;
1094    }
1095
1096    public Set<String> getNoPerDocumentQueryFacets() {
1097        return noPerDocumentQueryFacets;
1098    }
1099
1100    /**
1101     * Creates all the models.
1102     */
1103    private void initModels() {
1104        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
1105        log.debug("Schemas fields from descriptor: " + repositoryDescriptor.schemaFields);
1106        // document types
1107        for (DocumentType docType : schemaManager.getDocumentTypes()) {
1108            initDocTypeOrMixinModel(docType.getName(), docType.getSchemas(), allDocTypeSchemas, typeFragments,
1109                    typePropertyInfos, typeComplexChildren, true);
1110            initDocTypePrefetch(docType);
1111            initDocTypeMixins(docType);
1112            inferSuperType(docType);
1113        }
1114        // mixins
1115        for (CompositeType type : schemaManager.getFacets()) {
1116            initDocTypeOrMixinModel(type.getName(), type.getSchemas(), allMixinSchemas, mixinFragments,
1117                    mixinPropertyInfos, mixinComplexChildren, false);
1118            log.debug("Fragments for facet " + type.getName() + ": " + getMixinFragments(type.getName()));
1119        }
1120        // proxy schemas
1121        initProxySchemas(schemaManager.getProxySchemas(null));
1122        // second pass to get subtypes (needs all supertypes)
1123        for (DocumentType documentType : schemaManager.getDocumentTypes()) {
1124            inferSubTypes(documentType);
1125        }
1126        // init no per instance query facets
1127        initNoPerDocumentQueryFacets(schemaManager);
1128    }
1129
1130    private void initProxySchemas(List<Schema> proxySchemas) {
1131        Map<String, Set<String>> allSchemas = new HashMap<>();
1132        Map<String, Set<String>> allFragments = new HashMap<>();
1133        Map<String, Map<String, String>> allChildren = new HashMap<>();
1134        Map<String, Map<String, ModelProperty>> allPropertyInfos = new HashMap<>();
1135        String key = "__proxies__"; // not stored
1136        initDocTypeOrMixinModel(key, proxySchemas, allSchemas, allFragments, allPropertyInfos, allChildren, false);
1137        allProxySchemas.addAll(allSchemas.get(key));
1138        proxyFragments.addAll(allFragments.get(key));
1139        proxyPropertyInfos.putAll(allPropertyInfos.get(key));
1140        typeComplexChildren.put(PROXY_TYPE, allChildren.get(key));
1141    }
1142
1143    private Set<String> getCommonFragments(String typeName) {
1144        Set<String> fragments = new HashSet<>(5);
1145        fragments.add(VERSION_TABLE_NAME);
1146        fragments.add(ACL_TABLE_NAME);
1147        if (!miscInHierarchy) {
1148            fragments.add(MISC_TABLE_NAME);
1149        }
1150        if (fulltextConfiguration != null && fulltextConfiguration.isFulltextIndexable(typeName)) {
1151            fragments.add(FULLTEXT_TABLE_NAME);
1152        }
1153        return fragments;
1154    }
1155
1156    private Set<String> getCommonFragmentsPrefetched() {
1157        Set<String> fragments = new HashSet<>(5);
1158        fragments.add(VERSION_TABLE_NAME);
1159        fragments.add(ACL_TABLE_NAME);
1160        if (!miscInHierarchy) {
1161            fragments.add(MISC_TABLE_NAME);
1162        }
1163        return fragments;
1164    }
1165
1166    /**
1167     * For a doctype or mixin type, init the schemas-related structures.
1168     */
1169    private void initDocTypeOrMixinModel(String typeName, Collection<Schema> schemas,
1170            Map<String, Set<String>> schemasMap, Map<String, Set<String>> fragmentsMap,
1171            Map<String, Map<String, ModelProperty>> propertyInfoMap,
1172            Map<String, Map<String, String>> complexChildrenMap, boolean addCommonFragments) {
1173        Set<String> schemaNames = new HashSet<>();
1174        Set<String> fragmentNames = new HashSet<>();
1175        Map<String, String> complexChildren = new HashMap<>();
1176        if (addCommonFragments) {
1177            fragmentNames.addAll(getCommonFragments(typeName));
1178        }
1179        for (Schema schema : schemas) {
1180            if (schema == null) {
1181                // happens when a type refers to a nonexistent schema
1182                // TODO log and avoid nulls earlier
1183                continue;
1184            }
1185            schemaNames.add(schema.getName());
1186            try {
1187                fragmentNames.addAll(initSchemaModel(schema));
1188            } catch (NuxeoException e) {
1189                e.addInfo(String.format("Error initializing schema '%s' for composite type '%s'", schema.getName(),
1190                        typeName));
1191                throw e;
1192            }
1193            inferSchemaPropertyPaths(schema);
1194            complexChildren.putAll(schemaComplexChildren.get(schema.getName()));
1195        }
1196        schemasMap.put(typeName, schemaNames);
1197        fragmentsMap.put(typeName, fragmentNames);
1198        complexChildrenMap.put(typeName, complexChildren);
1199        inferPropertyInfos(propertyInfoMap, typeName, schemaNames);
1200    }
1201
1202    private void initDocTypePrefetch(DocumentType docType) {
1203        String docTypeName = docType.getName();
1204        PrefetchInfo prefetch = docType.getPrefetchInfo();
1205        if (prefetch != null) {
1206            Set<String> documentTypeFragments = getTypeFragments(docTypeName);
1207            for (String fieldName : prefetch.getFields()) {
1208                // prefetch all the relevant fragments
1209                // TODO deal with full xpath
1210                String fragment = fieldFragment.get(fieldName);
1211                if (fragment != null) {
1212                    // checks that the field actually belongs
1213                    // to the type
1214                    if (documentTypeFragments.contains(fragment)) {
1215                        addDocTypePrefetchedFragments(docTypeName, Collections.singleton(fragment));
1216                    }
1217                }
1218            }
1219            for (String schemaName : prefetch.getSchemas()) {
1220                Set<String> fragments = schemaFragments.get(schemaName);
1221                if (fragments != null) {
1222                    addDocTypePrefetchedFragments(docTypeName, fragments);
1223                }
1224            }
1225        }
1226        // always prefetch ACLs, versions, misc (for lifecycle)
1227        addDocTypePrefetchedFragments(docTypeName, getCommonFragmentsPrefetched());
1228
1229        log.debug("Fragments for type " + docTypeName + ": " + getTypeFragments(docTypeName) + ", prefetch: "
1230                + getTypePrefetchedFragments(docTypeName));
1231    }
1232
1233    private void initDocTypeMixins(DocumentType docType) {
1234        String docTypeName = docType.getName();
1235        Set<String> mixins = docType.getFacets();
1236        documentTypesMixins.put(docTypeName, new HashSet<>(mixins));
1237        for (String mixin : mixins) {
1238            mixinsDocumentTypes.computeIfAbsent(mixin, k -> new HashSet<>()).add(docTypeName);
1239        }
1240    }
1241
1242    private void inferSuperType(DocumentType docType) {
1243        Type superType = docType.getSuperType();
1244        if (superType != null) {
1245            documentSuperTypes.put(docType.getName(), superType.getName());
1246        }
1247    }
1248
1249    private void inferSubTypes(DocumentType docType) {
1250        String type = docType.getName();
1251        String superType = type;
1252        do {
1253            documentSubTypes.computeIfAbsent(superType, k -> new HashSet<>()).add(type);
1254            superType = documentSuperTypes.get(superType);
1255        } while (superType != null);
1256    }
1257
1258    private void initNoPerDocumentQueryFacets(SchemaManager schemaManager) {
1259        noPerDocumentQueryFacets.addAll(schemaManager.getNoPerDocumentQueryFacets());
1260    }
1261
1262    /**
1263     * Special model for the main table (the one containing the primary type information).
1264     * <p>
1265     * If the main table is not separate from the hierarchy table, then it's will not really be instantiated by itself
1266     * but merged into the hierarchy table.
1267     */
1268    private void initMainModel() {
1269        addPropertyInfo(MAIN_PRIMARY_TYPE_PROP, PropertyType.STRING, HIER_TABLE_NAME, MAIN_PRIMARY_TYPE_KEY, true, null,
1270                ColumnType.SYSNAME);
1271        addPropertyInfo(MAIN_MIXIN_TYPES_PROP, PropertyType.STRING, HIER_TABLE_NAME, MAIN_MIXIN_TYPES_KEY, false, null,
1272                ColumnType.SYSNAMEARRAY);
1273        addPropertyInfo(MAIN_CHECKED_IN_PROP, PropertyType.BOOLEAN, HIER_TABLE_NAME, MAIN_CHECKED_IN_KEY, false,
1274                BooleanType.INSTANCE, ColumnType.BOOLEAN);
1275        addPropertyInfo(MAIN_BASE_VERSION_PROP, idPropertyType, HIER_TABLE_NAME, MAIN_BASE_VERSION_KEY, false,
1276                idCoreType, ColumnType.NODEVAL);
1277        addPropertyInfo(MAIN_MAJOR_VERSION_PROP, PropertyType.LONG, HIER_TABLE_NAME, MAIN_MAJOR_VERSION_KEY, false,
1278                LongType.INSTANCE, ColumnType.INTEGER);
1279        addPropertyInfo(MAIN_MINOR_VERSION_PROP, PropertyType.LONG, HIER_TABLE_NAME, MAIN_MINOR_VERSION_KEY, false,
1280                LongType.INSTANCE, ColumnType.INTEGER);
1281        addPropertyInfo(MAIN_IS_VERSION_PROP, PropertyType.BOOLEAN, HIER_TABLE_NAME, MAIN_IS_VERSION_KEY, false,
1282                BooleanType.INSTANCE, ColumnType.BOOLEAN);
1283        addPropertyInfo(MAIN_IS_RETENTION_ACTIVE_PROP, PropertyType.BOOLEAN, HIER_TABLE_NAME,
1284                MAIN_IS_RETENTION_ACTIVE_KEY, false, BooleanType.INSTANCE, ColumnType.BOOLEAN);
1285        addPropertyInfo(MAIN_IS_TRASHED_PROP, PropertyType.BOOLEAN, HIER_TABLE_NAME, MAIN_IS_TRASHED_KEY, false,
1286                        BooleanType.INSTANCE, ColumnType.BOOLEAN);
1287        if (changeTokenEnabled) {
1288            addPropertyInfo(MAIN_SYS_CHANGE_TOKEN_PROP, PropertyType.LONG, HIER_TABLE_NAME, MAIN_SYS_CHANGE_TOKEN_KEY,
1289                    false, LongType.INSTANCE, ColumnType.LONG);
1290            addPropertyInfo(MAIN_CHANGE_TOKEN_PROP, PropertyType.LONG, HIER_TABLE_NAME, MAIN_CHANGE_TOKEN_KEY, false,
1291                    LongType.INSTANCE, ColumnType.LONG);
1292        }
1293        if (softDeleteEnabled) {
1294            addPropertyInfo(MAIN_IS_DELETED_PROP, PropertyType.BOOLEAN, HIER_TABLE_NAME, MAIN_IS_DELETED_KEY, true,
1295                    BooleanType.INSTANCE, ColumnType.BOOLEAN);
1296            addPropertyInfo(MAIN_DELETED_TIME_PROP, PropertyType.DATETIME, HIER_TABLE_NAME, MAIN_DELETED_TIME_KEY, true,
1297                    DateType.INSTANCE, ColumnType.TIMESTAMP);
1298        }
1299    }
1300
1301    /**
1302     * Special model for the "misc" table (lifecycle, dirty.).
1303     */
1304    private void initMiscModel() {
1305        String fragmentName = miscInHierarchy ? HIER_TABLE_NAME : MISC_TABLE_NAME;
1306        addPropertyInfo(MISC_LIFECYCLE_POLICY_PROP, PropertyType.STRING, fragmentName, MISC_LIFECYCLE_POLICY_KEY, false,
1307                StringType.INSTANCE, ColumnType.SYSNAME);
1308        addPropertyInfo(MISC_LIFECYCLE_STATE_PROP, PropertyType.STRING, fragmentName, MISC_LIFECYCLE_STATE_KEY, false,
1309                StringType.INSTANCE, ColumnType.SYSNAME);
1310    }
1311
1312    /**
1313     * Special model for the versions table.
1314     */
1315    private void initVersionsModel() {
1316        addPropertyInfo(VERSION_VERSIONABLE_PROP, idPropertyType, VERSION_TABLE_NAME, VERSION_VERSIONABLE_KEY, false,
1317                idCoreType, ColumnType.NODEVAL);
1318        addPropertyInfo(VERSION_CREATED_PROP, PropertyType.DATETIME, VERSION_TABLE_NAME, VERSION_CREATED_KEY, false,
1319                DateType.INSTANCE, ColumnType.TIMESTAMP);
1320        addPropertyInfo(VERSION_LABEL_PROP, PropertyType.STRING, VERSION_TABLE_NAME, VERSION_LABEL_KEY, false,
1321                StringType.INSTANCE, ColumnType.SYSNAME);
1322        addPropertyInfo(VERSION_DESCRIPTION_PROP, PropertyType.STRING, VERSION_TABLE_NAME, VERSION_DESCRIPTION_KEY,
1323                false, StringType.INSTANCE, ColumnType.STRING);
1324        addPropertyInfo(VERSION_IS_LATEST_PROP, PropertyType.BOOLEAN, VERSION_TABLE_NAME, VERSION_IS_LATEST_KEY, false,
1325                BooleanType.INSTANCE, ColumnType.BOOLEAN);
1326        addPropertyInfo(VERSION_IS_LATEST_MAJOR_PROP, PropertyType.BOOLEAN, VERSION_TABLE_NAME,
1327                VERSION_IS_LATEST_MAJOR_KEY, false, BooleanType.INSTANCE, ColumnType.BOOLEAN);
1328    }
1329
1330    /**
1331     * Special model for the proxies table.
1332     */
1333    private void initProxiesModel() {
1334        String type = PROXY_TYPE;
1335        addPropertyInfo(type, PROXY_TARGET_PROP, idPropertyType, PROXY_TABLE_NAME, PROXY_TARGET_KEY, false, idCoreType,
1336                ColumnType.NODEIDFKNP);
1337        addPropertyInfo(type, PROXY_VERSIONABLE_PROP, idPropertyType, PROXY_TABLE_NAME, PROXY_VERSIONABLE_KEY, false,
1338                idCoreType, ColumnType.NODEVAL);
1339        addTypeFragments(type, Collections.singleton(PROXY_TABLE_NAME));
1340    }
1341
1342    /**
1343     * Special model for the locks table (also, primary key has no foreign key, see {@link SQLInfo#initFragmentSQL}.
1344     */
1345    private void initLocksModel() {
1346        addPropertyInfo(LOCK_OWNER_PROP, PropertyType.STRING, LOCK_TABLE_NAME, LOCK_OWNER_KEY, false,
1347                StringType.INSTANCE, ColumnType.SYSNAME);
1348        addPropertyInfo(LOCK_CREATED_PROP, PropertyType.DATETIME, LOCK_TABLE_NAME, LOCK_CREATED_KEY, false,
1349                DateType.INSTANCE, ColumnType.TIMESTAMP);
1350    }
1351
1352    /**
1353     * Special model for the fulltext table.
1354     */
1355    private void initFullTextModel() {
1356        addPropertyInfo(FULLTEXT_JOBID_PROP, PropertyType.STRING, FULLTEXT_TABLE_NAME, FULLTEXT_JOBID_KEY, false,
1357                StringType.INSTANCE, ColumnType.SYSNAME);
1358        for (String indexName : fulltextConfiguration.indexNames) {
1359            String suffix = getFulltextIndexSuffix(indexName);
1360            if (materializeFulltextSyntheticColumn) {
1361                addPropertyInfo(FULLTEXT_FULLTEXT_PROP + suffix, PropertyType.STRING, FULLTEXT_TABLE_NAME,
1362                        FULLTEXT_FULLTEXT_KEY + suffix, false, StringType.INSTANCE, ColumnType.FTINDEXED);
1363            }
1364            addPropertyInfo(FULLTEXT_SIMPLETEXT_PROP + suffix, PropertyType.STRING, FULLTEXT_TABLE_NAME,
1365                    FULLTEXT_SIMPLETEXT_KEY + suffix, false, StringType.INSTANCE, ColumnType.FTSTORED);
1366            addPropertyInfo(FULLTEXT_BINARYTEXT_PROP + suffix, PropertyType.STRING, FULLTEXT_TABLE_NAME,
1367                    FULLTEXT_BINARYTEXT_KEY + suffix, false, StringType.INSTANCE, ColumnType.FTSTORED);
1368        }
1369    }
1370
1371    public String getFulltextIndexSuffix(String indexName) {
1372        return indexName.equals(FULLTEXT_DEFAULT_INDEX) ? "" : '_' + indexName;
1373    }
1374
1375    /**
1376     * Special collection-like model for the ACL table.
1377     */
1378    private void initAclModel() {
1379        Map<String, ColumnType> keysType = new LinkedHashMap<>();
1380        keysType.put(ACL_POS_KEY, ColumnType.INTEGER);
1381        keysType.put(ACL_NAME_KEY, ColumnType.SYSNAME);
1382        keysType.put(ACL_GRANT_KEY, ColumnType.BOOLEAN);
1383        keysType.put(ACL_PERMISSION_KEY, ColumnType.SYSNAME);
1384        keysType.put(ACL_CREATOR_KEY, ColumnType.SYSNAME);
1385        keysType.put(ACL_BEGIN_KEY, ColumnType.TIMESTAMP);
1386        keysType.put(ACL_END_KEY, ColumnType.TIMESTAMP);
1387        keysType.put(ACL_STATUS_KEY, ColumnType.LONG);
1388        keysType.put(ACL_USER_KEY, ColumnType.SYSNAME);
1389        keysType.put(ACL_GROUP_KEY, ColumnType.SYSNAME);
1390        String fragmentName = ACL_TABLE_NAME;
1391        addCollectionFragmentInfos(fragmentName, PropertyType.COLL_ACL, ACL_POS_KEY, keysType);
1392        addPropertyInfo(ACL_PROP, PropertyType.COLL_ACL, fragmentName, null, false, null, null);
1393        // for query
1394        // composed of NXQL.ECM_ACL and NXQL.ECM_ACL_PRINCIPAL etc.
1395        allPathPropertyInfos.put("ecm:acl.principal/*",
1396                new ModelProperty(PropertyType.STRING, fragmentName, ACL_USER_KEY, true));
1397        allPathPropertyInfos.put("ecm:acl.permission/*",
1398                new ModelProperty(PropertyType.STRING, fragmentName, ACL_PERMISSION_KEY, true));
1399        allPathPropertyInfos.put("ecm:acl.grant/*",
1400                new ModelProperty(PropertyType.BOOLEAN, fragmentName, ACL_GRANT_KEY, true));
1401        allPathPropertyInfos.put("ecm:acl.name/*",
1402                new ModelProperty(PropertyType.STRING, fragmentName, ACL_NAME_KEY, true));
1403        allPathPropertyInfos.put("ecm:acl.pos/*",
1404                new ModelProperty(PropertyType.LONG, fragmentName, ACL_POS_KEY, true));
1405        allPathPropertyInfos.put("ecm:acl.creator/*",
1406                new ModelProperty(PropertyType.STRING, fragmentName, ACL_CREATOR_KEY, true));
1407        allPathPropertyInfos.put("ecm:acl.begin/*",
1408                new ModelProperty(PropertyType.DATETIME, fragmentName, ACL_BEGIN_KEY, true));
1409        allPathPropertyInfos.put("ecm:acl.end/*",
1410                new ModelProperty(PropertyType.DATETIME, fragmentName, ACL_END_KEY, true));
1411        allPathPropertyInfos.put("ecm:acl.status/*",
1412                new ModelProperty(PropertyType.LONG, fragmentName, ACL_STATUS_KEY, true));
1413    }
1414
1415    /**
1416     * Creates the model for a schema.
1417     */
1418    private Set<String> initSchemaModel(Schema schema) {
1419        initComplexTypeModel(schema);
1420        return schemaFragments.get(schema.getName());
1421    }
1422
1423    /**
1424     * Creates the model for a complex type or a schema. Recurses in complex types.
1425     * <p>
1426     * Adds the simple+collection fragments to {@link #typeFragments} or {@link #schemaFragments}.
1427     */
1428    private void initComplexTypeModel(ComplexType complexType) {
1429        String typeName = complexType.getName();
1430        boolean isSchema = complexType instanceof Schema;
1431        if (isSchema && schemaFragments.containsKey(typeName)) {
1432            return;
1433        } else if (!isSchema && typeFragments.containsKey(typeName)) {
1434            return;
1435        }
1436        /** The fragment names to use for this type, usually just one. */
1437        Set<String> fragmentNames = new HashSet<>(1);
1438        /** The children complex properties for this type. */
1439        Map<String, String> complexChildren = new HashMap<>(1);
1440
1441        log.debug("Making model for type " + typeName);
1442
1443        /** Initialized if this type has a table associated. */
1444        for (Field field : complexType.getFields()) {
1445            Type fieldType = field.getType();
1446            if (fieldType.isComplexType()) {
1447                /*
1448                 * Complex type.
1449                 */
1450                String propertyName = field.getName().getPrefixedName();
1451                complexChildren.put(propertyName, fieldType.getName());
1452                initComplexTypeModel((ComplexType) fieldType);
1453            } else {
1454                String propertyName = field.getName().getPrefixedName();
1455                FieldDescriptor fieldDescriptor = null;
1456                for (FieldDescriptor fd : repositoryDescriptor.schemaFields) {
1457                    if (propertyName.equals(fd.field)) {
1458                        fieldDescriptor = fd;
1459                        break;
1460                    }
1461                }
1462                if (fieldType.isListType()) {
1463                    Type listFieldType = ((ListType) fieldType).getFieldType();
1464                    if (listFieldType.isSimpleType()) {
1465                        /*
1466                         * Simple list.
1467                         */
1468                        PropertyType propertyType = PropertyType.fromFieldType(listFieldType, true);
1469                        boolean useArray = false;
1470                        ColumnType columnType = null;
1471                        if (repositoryDescriptor.getArrayColumns() && fieldDescriptor == null) {
1472                            fieldDescriptor = new FieldDescriptor();
1473                            fieldDescriptor.type = FIELD_TYPE_ARRAY;
1474                        }
1475                        if (fieldDescriptor != null) {
1476                            if (FIELD_TYPE_ARRAY.equals(fieldDescriptor.type)) {
1477                                if (!supportsArrayColumns) {
1478                                    log.warn("  Field '" + propertyName + "' array specification is ignored since"
1479                                            + " this database does not support arrays");
1480                                }
1481                                useArray = supportsArrayColumns;
1482                                columnType = ColumnType.fromFieldType(listFieldType, useArray);
1483                            } else if (FIELD_TYPE_ARRAY_LARGETEXT.equals(fieldDescriptor.type)) {
1484                                boolean isStringColSpec = ColumnType.fromFieldType(
1485                                        listFieldType).spec == ColumnSpec.STRING;
1486                                if (supportsArrayColumns && !isStringColSpec) {
1487                                    log.warn("  Field '" + propertyName + "' is not a String yet it is specified"
1488                                            + " as array_largetext, using ARRAY_CLOB for it");
1489                                } else if (!supportsArrayColumns && isStringColSpec) {
1490                                    log.warn("  Field '" + propertyName + "' array specification is ignored since"
1491                                            + " this database does not support arrays," + " using CLOB for it");
1492                                } else if (!supportsArrayColumns && !isStringColSpec) {
1493                                    log.warn("  Field '" + propertyName + "' array specification is ignored since"
1494                                            + " this database does not support arrays, also"
1495                                            + " Field is not a String yet it is specified"
1496                                            + " as array_largetext, using CLOB for it");
1497                                }
1498                                useArray = supportsArrayColumns;
1499                                columnType = (supportsArrayColumns) ? ColumnType.ARRAY_CLOB : ColumnType.CLOB;
1500                            } else if (FIELD_TYPE_LARGETEXT.equals(fieldDescriptor.type)) {
1501                                if (ColumnType.fromFieldType(listFieldType).spec != ColumnSpec.STRING) {
1502                                    log.warn("  Field '" + propertyName + "' is not a String yet it is specified "
1503                                            + " as largetext, using CLOB for it");
1504                                }
1505                                columnType = ColumnType.CLOB;
1506                            } else {
1507                                log.warn("  Field '" + propertyName + "' specified but not successfully mapped");
1508                            }
1509                        }
1510
1511                        if (columnType == null) {
1512                            columnType = ColumnType.fromFieldType(listFieldType);
1513                        }
1514                        log.debug("  List field '" + propertyName + "' using column type " + columnType);
1515
1516                        if (useArray) {
1517                            /*
1518                             * Array: use an array.
1519                             */
1520                            String fragmentName = typeFragmentName(complexType);
1521                            String fragmentKey = field.getName().getLocalName();
1522                            addPropertyInfo(complexType, propertyName, propertyType, fragmentName, fragmentKey, false,
1523                                    null, columnType);
1524                            addFieldFragment(field, fragmentName);
1525                        } else {
1526                            /*
1527                             * Array: use a collection table.
1528                             */
1529                            String fragmentName = collectionFragmentName(propertyName);
1530                            addPropertyInfo(complexType, propertyName, propertyType, fragmentName, COLL_TABLE_VALUE_KEY,
1531                                    false, null, columnType);
1532                            // pseudo-syntax with ending "#" to get to the pos column
1533                            String posPropertyName = propertyName + "#";
1534                            PropertyType posPropertyType = PropertyType.LONG;
1535                            addPropertyInfo(complexType, posPropertyName, posPropertyType, fragmentName,
1536                                    COLL_TABLE_POS_KEY, false, null, ColumnType.INTEGER);
1537
1538                            Map<String, ColumnType> keysType = new LinkedHashMap<>();
1539                            keysType.put(COLL_TABLE_POS_KEY, ColumnType.INTEGER);
1540                            keysType.put(COLL_TABLE_VALUE_KEY, columnType);
1541                            addCollectionFragmentInfos(fragmentName, propertyType, COLL_TABLE_POS_KEY, keysType);
1542
1543                            fragmentNames.add(fragmentName);
1544                            addFieldFragment(field, fragmentName);
1545                        }
1546                    } else {
1547                        /*
1548                         * Complex list.
1549                         */
1550                        initComplexTypeModel((ComplexType) listFieldType);
1551                    }
1552                } else {
1553                    /*
1554                     * Primitive type.
1555                     */
1556                    String fragmentName = typeFragmentName(complexType);
1557                    String fragmentKey = field.getName().getLocalName();
1558                    PropertyType propertyType = PropertyType.fromFieldType(fieldType, false);
1559                    ColumnType type = ColumnType.fromField(field);
1560                    if (type.spec == ColumnSpec.STRING) {
1561                        // backward compat with largetext, since 5.4.2
1562                        if (fieldDescriptor != null && FIELD_TYPE_LARGETEXT.equals(fieldDescriptor.type)) {
1563                            if (!type.isUnconstrained() && !type.isClob()) {
1564                                log.warn("  String field '" + propertyName + "' has a schema constraint to " + type
1565                                        + " but is specified as largetext," + " using CLOB for it");
1566                            }
1567                            type = ColumnType.CLOB;
1568                        }
1569                    }
1570                    if (fieldDescriptor != null) {
1571                        if (fieldDescriptor.table != null) {
1572                            fragmentName = fieldDescriptor.table;
1573                        }
1574                        if (fieldDescriptor.column != null) {
1575                            fragmentKey = fieldDescriptor.column;
1576                        }
1577                    }
1578                    fragmentKey = checkReservedName(fragmentKey);
1579                    if (fragmentName.equals(UID_SCHEMA_NAME) && (fragmentKey.equals(UID_MAJOR_VERSION_KEY)
1580                            || fragmentKey.equals(UID_MINOR_VERSION_KEY))) {
1581                        // workaround: special-case the "uid" schema, put
1582                        // major/minor in the hierarchy table
1583                        fragmentName = HIER_TABLE_NAME;
1584                        fragmentKey = fragmentKey.equals(UID_MAJOR_VERSION_KEY) ? MAIN_MAJOR_VERSION_KEY
1585                                : MAIN_MINOR_VERSION_KEY;
1586                    }
1587                    addPropertyInfo(complexType, propertyName, propertyType, fragmentName, fragmentKey, false, null,
1588                            type);
1589                    if (!fragmentName.equals(HIER_TABLE_NAME)) {
1590                        fragmentNames.add(fragmentName);
1591                        addFieldFragment(field, fragmentName);
1592                    }
1593                }
1594            }
1595        }
1596
1597        if (isSchema) {
1598            schemaFragments.put(typeName, fragmentNames);
1599            schemaComplexChildren.put(typeName, complexChildren);
1600        } else {
1601            addTypeFragments(typeName, fragmentNames);
1602            typeComplexChildren.put(typeName, complexChildren);
1603        }
1604    }
1605
1606    protected String checkReservedName(String key) {
1607        String k = key.replaceAll("_+$", "");
1608        if (k.equalsIgnoreCase(MAIN_KEY)) {
1609            // reserved, add one more suffix "_"
1610            key += "_";
1611        }
1612        return key;
1613    }
1614
1615    private static String typeFragmentName(ComplexType type) {
1616        return type.getName();
1617    }
1618
1619    private static String collectionFragmentName(String propertyName) {
1620        return propertyName;
1621    }
1622}