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