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