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