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