001/*
002 * (C) Copyright 2014 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 */
019package org.nuxeo.ecm.core.storage.dbs;
020
021import static java.lang.Boolean.TRUE;
022import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_BEGIN;
023import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_CREATOR;
024import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_END;
025import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_GRANT;
026import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_PERMISSION;
027import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_STATUS;
028import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_USER;
029import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL;
030import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL_NAME;
031import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACP;
032import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ANCESTOR_IDS;
033import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_BASE_VERSION_ID;
034import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_BINARY;
035import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_JOBID;
036import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_SCORE;
037import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_SIMPLE;
038import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID;
039import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_CHECKED_IN;
040import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_LATEST_MAJOR_VERSION;
041import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_LATEST_VERSION;
042import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_PROXY;
043import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_VERSION;
044import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_LIFECYCLE_POLICY;
045import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_LIFECYCLE_STATE;
046import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_LOCK_CREATED;
047import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_LOCK_OWNER;
048import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MAJOR_VERSION;
049import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MINOR_VERSION;
050import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MIXIN_TYPES;
051import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_NAME;
052import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PARENT_ID;
053import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PATH_INTERNAL;
054import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_POS;
055import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PRIMARY_TYPE;
056import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_IDS;
057import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_TARGET_ID;
058import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_VERSION_SERIES_ID;
059import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_READ_ACL;
060import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_CREATED;
061import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_DESCRIPTION;
062import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_LABEL;
063import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_SERIES_ID;
064
065import java.io.Serializable;
066import java.text.DateFormat;
067import java.text.Normalizer;
068import java.text.ParseException;
069import java.util.ArrayList;
070import java.util.Calendar;
071import java.util.Collections;
072import java.util.Comparator;
073import java.util.GregorianCalendar;
074import java.util.HashMap;
075import java.util.HashSet;
076import java.util.Iterator;
077import java.util.LinkedList;
078import java.util.List;
079import java.util.Map;
080import java.util.Map.Entry;
081import java.util.NoSuchElementException;
082import java.util.Set;
083import java.util.regex.Matcher;
084import java.util.regex.Pattern;
085
086import org.apache.commons.lang.ObjectUtils;
087import org.apache.commons.lang.StringUtils;
088import org.nuxeo.ecm.core.api.CoreSession;
089import org.nuxeo.ecm.core.api.DocumentExistsException;
090import org.nuxeo.ecm.core.api.DocumentNotFoundException;
091import org.nuxeo.ecm.core.api.IterableQueryResult;
092import org.nuxeo.ecm.core.api.NuxeoException;
093import org.nuxeo.ecm.core.api.PartialList;
094import org.nuxeo.ecm.core.api.VersionModel;
095import org.nuxeo.ecm.core.api.security.ACE;
096import org.nuxeo.ecm.core.api.security.ACL;
097import org.nuxeo.ecm.core.api.security.ACP;
098import org.nuxeo.ecm.core.api.security.Access;
099import org.nuxeo.ecm.core.api.security.SecurityConstants;
100import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
101import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
102import org.nuxeo.ecm.core.blob.BlobManager;
103import org.nuxeo.ecm.core.model.Document;
104import org.nuxeo.ecm.core.model.LockManager;
105import org.nuxeo.ecm.core.model.Session;
106import org.nuxeo.ecm.core.query.QueryFilter;
107import org.nuxeo.ecm.core.query.QueryParseException;
108import org.nuxeo.ecm.core.query.sql.NXQL;
109import org.nuxeo.ecm.core.query.sql.SQLQueryParser;
110import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
111import org.nuxeo.ecm.core.query.sql.model.OrderByClause;
112import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
113import org.nuxeo.ecm.core.query.sql.model.OrderByList;
114import org.nuxeo.ecm.core.query.sql.model.Reference;
115import org.nuxeo.ecm.core.query.sql.model.SQLQuery;
116import org.nuxeo.ecm.core.query.sql.model.SelectClause;
117import org.nuxeo.ecm.core.schema.DocumentType;
118import org.nuxeo.ecm.core.schema.FacetNames;
119import org.nuxeo.ecm.core.schema.SchemaManager;
120import org.nuxeo.ecm.core.schema.types.ListTypeImpl;
121import org.nuxeo.ecm.core.schema.types.Type;
122import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
123import org.nuxeo.ecm.core.schema.types.primitives.DateType;
124import org.nuxeo.ecm.core.schema.types.primitives.StringType;
125import org.nuxeo.ecm.core.storage.ExpressionEvaluator;
126import org.nuxeo.ecm.core.storage.QueryOptimizer;
127import org.nuxeo.ecm.core.storage.State;
128import org.nuxeo.ecm.core.storage.StateHelper;
129import org.nuxeo.runtime.api.Framework;
130import org.nuxeo.runtime.transaction.TransactionHelper;
131
132/**
133 * Implementation of a {@link Session} for Document-Based Storage.
134 *
135 * @since 5.9.4
136 */
137public class DBSSession implements Session {
138
139    protected final DBSRepository repository;
140
141    protected final DBSTransactionState transaction;
142
143    protected boolean closed;
144
145    public DBSSession(DBSRepository repository) {
146        this.repository = repository;
147        transaction = new DBSTransactionState(repository, this);
148    }
149
150    @Override
151    public String getRepositoryName() {
152        return repository.getName();
153    }
154
155    @Override
156    public void close() {
157        closed = true;
158    }
159
160    @Override
161    public boolean isLive() {
162        return !closed;
163    }
164
165    @Override
166    public void save() {
167        transaction.save();
168        if (!TransactionHelper.isTransactionActive()) {
169            transaction.commit();
170        }
171    }
172
173    public void begin() {
174        transaction.begin();
175    }
176
177    public void commit() {
178        transaction.commit();
179    }
180
181    public void rollback() {
182        transaction.rollback();
183    }
184
185    @Override
186    public boolean isStateSharedByAllThreadSessions() {
187        return false;
188    }
189
190    protected BlobManager getBlobManager() {
191        return repository.getBlobManager();
192    }
193
194    protected String getRootId() {
195        return repository.getRootId();
196    }
197
198    /*
199     * Normalize using NFC to avoid decomposed characters (like 'e' + COMBINING ACUTE ACCENT instead of LATIN SMALL
200     * LETTER E WITH ACUTE). NFKC (normalization using compatibility decomposition) is not used, because compatibility
201     * decomposition turns some characters (LATIN SMALL LIGATURE FFI, TRADE MARK SIGN, FULLWIDTH SOLIDUS) into a series
202     * of characters ('f'+'f'+'i', 'T'+'M', '/') that cannot be re-composed into the original, and therefore loses
203     * information.
204     */
205    protected String normalize(String path) {
206        return Normalizer.normalize(path, Normalizer.Form.NFC);
207    }
208
209    @Override
210    public Document resolvePath(String path) {
211        // TODO move checks and normalize higher in call stack
212        if (path == null) {
213            throw new DocumentNotFoundException("Null path");
214        }
215        int len = path.length();
216        if (len == 0) {
217            throw new DocumentNotFoundException("Empty path");
218        }
219        if (path.charAt(0) != '/') {
220            throw new DocumentNotFoundException("Relative path: " + path);
221        }
222        if (len > 1 && path.charAt(len - 1) == '/') {
223            // remove final slash
224            path = path.substring(0, len - 1);
225            len--;
226        }
227        path = normalize(path);
228
229        if (len == 1) {
230            return getRootDocument();
231        }
232        DBSDocumentState docState = null;
233        String parentId = getRootId();
234        String[] names = path.split("/", -1);
235        for (int i = 1; i < names.length; i++) {
236            String name = names[i];
237            if (name.length() == 0) {
238                throw new DocumentNotFoundException("Path with empty component: " + path);
239            }
240            docState = transaction.getChildState(parentId, name);
241            if (docState == null) {
242                throw new DocumentNotFoundException(path);
243            }
244            parentId = docState.getId();
245        }
246        return getDocument(docState);
247    }
248
249    protected String getDocumentIdByPath(String path) {
250        // TODO move checks and normalize higher in call stack
251        if (path == null) {
252            throw new DocumentNotFoundException("Null path");
253        }
254        int len = path.length();
255        if (len == 0) {
256            throw new DocumentNotFoundException("Empty path");
257        }
258        if (path.charAt(0) != '/') {
259            throw new DocumentNotFoundException("Relative path: " + path);
260        }
261        if (len > 1 && path.charAt(len - 1) == '/') {
262            // remove final slash
263            path = path.substring(0, len - 1);
264            len--;
265        }
266        path = normalize(path);
267
268        if (len == 1) {
269            return getRootId();
270        }
271        DBSDocumentState docState = null;
272        String parentId = getRootId();
273        String[] names = path.split("/", -1);
274        for (int i = 1; i < names.length; i++) {
275            String name = names[i];
276            if (name.length() == 0) {
277                throw new DocumentNotFoundException("Path with empty component: " + path);
278            }
279            // TODO XXX add getChildId method
280            docState = transaction.getChildState(parentId, name);
281            if (docState == null) {
282                return null;
283            }
284            parentId = docState.getId();
285        }
286        return docState.getId();
287    }
288
289    protected Document getChild(String parentId, String name) {
290        name = normalize(name);
291        DBSDocumentState docState = transaction.getChildState(parentId, name);
292        return getDocument(docState);
293    }
294
295    protected List<Document> getChildren(String parentId) {
296        List<DBSDocumentState> docStates = transaction.getChildrenStates(parentId);
297        if (isOrderable(parentId)) {
298            // sort children in order
299            Collections.sort(docStates, POS_COMPARATOR);
300        }
301        List<Document> children = new ArrayList<Document>(docStates.size());
302        for (DBSDocumentState docState : docStates) {
303            try {
304                children.add(getDocument(docState));
305            } catch (DocumentNotFoundException e) {
306                // ignore error retrieving one of the children
307                // (Unknown document type)
308                continue;
309            }
310        }
311        return children;
312    }
313
314    protected List<String> getChildrenIds(String parentId) {
315        if (isOrderable(parentId)) {
316            // TODO get only id and pos, not full state
317            // TODO state not for update
318            List<DBSDocumentState> docStates = transaction.getChildrenStates(parentId);
319            Collections.sort(docStates, POS_COMPARATOR);
320            List<String> children = new ArrayList<String>(docStates.size());
321            for (DBSDocumentState docState : docStates) {
322                children.add(docState.getId());
323            }
324            return children;
325        } else {
326            return transaction.getChildrenIds(parentId);
327        }
328    }
329
330    protected boolean hasChildren(String parentId) {
331        return transaction.hasChildren(parentId);
332
333    }
334
335    @Override
336    public Document getDocumentByUUID(String id) {
337        Document doc = getDocument(id);
338        if (doc != null) {
339            return doc;
340        }
341        // exception required by API
342        throw new DocumentNotFoundException(id);
343    }
344
345    @Override
346    public Document getRootDocument() {
347        return getDocument(getRootId());
348    }
349
350    @Override
351    public Document getNullDocument() {
352        return new DBSDocument(null, null, this, true);
353    }
354
355    protected DBSDocument getDocument(String id) {
356        DBSDocumentState docState = transaction.getStateForUpdate(id);
357        return getDocument(docState);
358    }
359
360    protected List<Document> getDocuments(List<String> ids) {
361        List<DBSDocumentState> docStates = transaction.getStatesForUpdate(ids);
362        List<Document> docs = new ArrayList<Document>(ids.size());
363        for (DBSDocumentState docState : docStates) {
364            docs.add(getDocument(docState));
365        }
366        return docs;
367    }
368
369    protected DBSDocument getDocument(DBSDocumentState docState) {
370        return getDocument(docState, true);
371    }
372
373    protected DBSDocument getDocument(DBSDocumentState docState, boolean readonly) {
374        if (docState == null) {
375            return null;
376        }
377        boolean isVersion = TRUE.equals(docState.get(KEY_IS_VERSION));
378
379        String typeName = docState.getPrimaryType();
380        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
381        DocumentType type = schemaManager.getDocumentType(typeName);
382        if (type == null) {
383            throw new DocumentNotFoundException("Unknown document type: " + typeName);
384        }
385
386        if (isVersion) {
387            return new DBSDocument(docState, type, this, readonly);
388        } else {
389            return new DBSDocument(docState, type, this, false);
390        }
391    }
392
393    protected boolean hasChild(String parentId, String name) {
394        name = normalize(name);
395        return transaction.hasChild(parentId, name);
396    }
397
398    public Document createChild(String id, String parentId, String name, Long pos, String typeName) {
399        name = normalize(name);
400        DBSDocumentState docState = createChildState(id, parentId, name, pos, typeName);
401        return getDocument(docState);
402    }
403
404    protected DBSDocumentState createChildState(String id, String parentId, String name, Long pos, String typeName) {
405        if (pos == null && parentId != null) {
406            pos = getNextPos(parentId);
407        }
408        return transaction.createChild(id, parentId, name, pos, typeName);
409    }
410
411    protected boolean isOrderable(String id) {
412        State state = transaction.getStateForRead(id);
413        String typeName = (String) state.get(KEY_PRIMARY_TYPE);
414        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
415        return schemaManager.getDocumentType(typeName).getFacets().contains(FacetNames.ORDERABLE);
416    }
417
418    protected Long getNextPos(String parentId) {
419        if (!isOrderable(parentId)) {
420            return null;
421        }
422        long max = -1;
423        for (DBSDocumentState docState : transaction.getChildrenStates(parentId)) {
424            Long pos = (Long) docState.get(KEY_POS);
425            if (pos != null && pos.longValue() > max) {
426                max = pos.longValue();
427            }
428        }
429        return Long.valueOf(max + 1);
430    }
431
432    protected void orderBefore(String parentId, String sourceId, String destId) {
433        if (!isOrderable(parentId)) {
434            // TODO throw exception?
435            return;
436        }
437        if (sourceId.equals(destId)) {
438            return;
439        }
440        // This is optimized by assuming the number of children is small enough
441        // to be manageable in-memory.
442        // fetch children
443        List<DBSDocumentState> docStates = transaction.getChildrenStates(parentId);
444        // sort children in order
445        Collections.sort(docStates, POS_COMPARATOR);
446        // renumber
447        int i = 0;
448        DBSDocumentState source = null; // source if seen
449        Long destPos = null;
450        for (DBSDocumentState docState : docStates) {
451            Serializable id = docState.getId();
452            if (id.equals(destId)) {
453                destPos = Long.valueOf(i);
454                i++;
455                if (source != null) {
456                    source.put(KEY_POS, destPos);
457                }
458            }
459            Long setPos;
460            if (id.equals(sourceId)) {
461                i--;
462                source = docState;
463                setPos = destPos;
464            } else {
465                setPos = Long.valueOf(i);
466            }
467            if (setPos != null) {
468                if (!setPos.equals(docState.get(KEY_POS))) {
469                    docState.put(KEY_POS, setPos);
470                }
471            }
472            i++;
473        }
474        if (destId == null) {
475            Long setPos = Long.valueOf(i);
476            if (!setPos.equals(source.get(KEY_POS))) {
477                source.put(KEY_POS, setPos);
478            }
479        }
480    }
481
482    protected void checkOut(String id) {
483        DBSDocumentState docState = transaction.getStateForUpdate(id);
484        if (!TRUE.equals(docState.get(KEY_IS_CHECKED_IN))) {
485            throw new NuxeoException("Already checked out");
486        }
487        docState.put(KEY_IS_CHECKED_IN, null);
488    }
489
490    protected Document checkIn(String id, String label, String checkinComment) {
491        transaction.save();
492        DBSDocumentState docState = transaction.getStateForUpdate(id);
493        if (TRUE.equals(docState.get(KEY_IS_CHECKED_IN))) {
494            throw new NuxeoException("Already checked in");
495        }
496        if (label == null) {
497            // use version major + minor as label
498            Long major = (Long) docState.get(KEY_MAJOR_VERSION);
499            Long minor = (Long) docState.get(KEY_MINOR_VERSION);
500            if (major == null || minor == null) {
501                label = "";
502            } else {
503                label = major + "." + minor;
504            }
505        }
506
507        // copy into a version
508        DBSDocumentState verState = transaction.copy(id);
509        String verId = verState.getId();
510        verState.put(KEY_PARENT_ID, null);
511        verState.put(KEY_ANCESTOR_IDS, null);
512        verState.put(KEY_IS_VERSION, TRUE);
513        verState.put(KEY_VERSION_SERIES_ID, id);
514        verState.put(KEY_VERSION_CREATED, new GregorianCalendar()); // now
515        verState.put(KEY_VERSION_LABEL, label);
516        verState.put(KEY_VERSION_DESCRIPTION, checkinComment);
517        verState.put(KEY_IS_LATEST_VERSION, TRUE);
518        verState.put(KEY_IS_CHECKED_IN, null);
519        verState.put(KEY_BASE_VERSION_ID, null);
520        boolean isMajor = Long.valueOf(0).equals(verState.get(KEY_MINOR_VERSION));
521        verState.put(KEY_IS_LATEST_MAJOR_VERSION, isMajor ? TRUE : null);
522
523        // update the doc to mark it checked in
524        docState.put(KEY_IS_CHECKED_IN, TRUE);
525        docState.put(KEY_BASE_VERSION_ID, verId);
526
527        recomputeVersionSeries(id);
528        transaction.save();
529
530        return getDocument(verId);
531    }
532
533    /**
534     * Recomputes isLatest / isLatestMajor on all versions.
535     */
536    protected void recomputeVersionSeries(String versionSeriesId) {
537        List<DBSDocumentState> docStates = transaction.getKeyValuedStates(KEY_VERSION_SERIES_ID, versionSeriesId,
538                KEY_IS_VERSION, TRUE);
539        Collections.sort(docStates, VERSION_CREATED_COMPARATOR);
540        Collections.reverse(docStates);
541        boolean isLatest = true;
542        boolean isLatestMajor = true;
543        for (DBSDocumentState docState : docStates) {
544            // isLatestVersion
545            docState.put(KEY_IS_LATEST_VERSION, isLatest ? TRUE : null);
546            isLatest = false;
547            // isLatestMajorVersion
548            boolean isMajor = Long.valueOf(0).equals(docState.get(KEY_MINOR_VERSION));
549            docState.put(KEY_IS_LATEST_MAJOR_VERSION, isMajor && isLatestMajor ? TRUE : null);
550            if (isMajor) {
551                isLatestMajor = false;
552            }
553        }
554    }
555
556    protected void restoreVersion(Document doc, Document version) {
557        String docId = doc.getUUID();
558        String versionId = version.getUUID();
559
560        DBSDocumentState docState = transaction.getStateForUpdate(docId);
561        State versionState = transaction.getStateForRead(versionId);
562
563        // clear all data
564        for (String key : docState.state.keyArray()) {
565            if (!keepWhenRestore(key)) {
566                docState.put(key, null);
567            }
568        }
569        // update from version
570        for (Entry<String, Serializable> en : versionState.entrySet()) {
571            String key = en.getKey();
572            if (!keepWhenRestore(key)) {
573                docState.put(key, StateHelper.deepCopy(en.getValue()));
574            }
575        }
576        docState.put(KEY_IS_VERSION, null);
577        docState.put(KEY_IS_CHECKED_IN, TRUE);
578        docState.put(KEY_BASE_VERSION_ID, versionId);
579    }
580
581    // keys we don't copy from version when restoring
582    protected boolean keepWhenRestore(String key) {
583        switch (key) {
584        // these are placeful stuff
585        case KEY_ID:
586        case KEY_PARENT_ID:
587        case KEY_ANCESTOR_IDS:
588        case KEY_NAME:
589        case KEY_POS:
590        case KEY_PRIMARY_TYPE:
591        case KEY_ACP:
592        case KEY_READ_ACL:
593            // these are version-specific
594        case KEY_VERSION_CREATED:
595        case KEY_VERSION_DESCRIPTION:
596        case KEY_VERSION_LABEL:
597        case KEY_VERSION_SERIES_ID:
598        case KEY_IS_LATEST_VERSION:
599        case KEY_IS_LATEST_MAJOR_VERSION:
600            // these will be updated after restore
601        case KEY_IS_VERSION:
602        case KEY_IS_CHECKED_IN:
603        case KEY_BASE_VERSION_ID:
604            return true;
605        }
606        return false;
607    }
608
609    @Override
610    public Document copy(Document source, Document parent, String name) {
611        transaction.save();
612        if (name == null) {
613            name = source.getName();
614        }
615        name = findFreeName(parent, name);
616        String sourceId = source.getUUID();
617        String parentId = parent.getUUID();
618        State sourceState = transaction.getStateForRead(sourceId);
619        State parentState = transaction.getStateForRead(parentId);
620        String oldParentId = (String) sourceState.get(KEY_PARENT_ID);
621        Object[] parentAncestorIds = (Object[]) parentState.get(KEY_ANCESTOR_IDS);
622        LinkedList<String> ancestorIds = new LinkedList<String>();
623        if (parentAncestorIds != null) {
624            for (Object id : parentAncestorIds) {
625                ancestorIds.add((String) id);
626            }
627        }
628        ancestorIds.add(parentId);
629        if (oldParentId != null && !oldParentId.equals(parentId)) {
630            if (ancestorIds.contains(sourceId)) {
631                throw new DocumentExistsException("Cannot copy a node under itself: " + parentId + " is under " + sourceId);
632
633            }
634            // checkNotUnder(parentId, sourceId, "copy");
635        }
636        // do the copy
637        Long pos = getNextPos(parentId);
638        String copyId = copyRecurse(sourceId, parentId, ancestorIds, name);
639        DBSDocumentState copyState = transaction.getStateForUpdate(copyId);
640        // version copy fixup
641        if (source.isVersion()) {
642            copyState.put(KEY_IS_VERSION, null);
643        }
644        // pos fixup
645        copyState.put(KEY_POS, pos);
646        // update read acls
647        transaction.updateReadAcls(copyId);
648
649        return getDocument(copyState);
650    }
651
652    protected String copyRecurse(String sourceId, String parentId, LinkedList<String> ancestorIds, String name) {
653        String copyId = copy(sourceId, parentId, ancestorIds, name);
654        ancestorIds.addLast(copyId);
655        for (String childId : getChildrenIds(sourceId)) {
656            copyRecurse(childId, copyId, ancestorIds, null);
657        }
658        ancestorIds.removeLast();
659        return copyId;
660    }
661
662    /**
663     * Copy source under parent, and set its ancestors.
664     */
665    protected String copy(String sourceId, String parentId, List<String> ancestorIds, String name) {
666        DBSDocumentState copy = transaction.copy(sourceId);
667        copy.put(KEY_PARENT_ID, parentId);
668        copy.put(KEY_ANCESTOR_IDS, ancestorIds.toArray(new Object[ancestorIds.size()]));
669        if (name != null) {
670            copy.put(KEY_NAME, name);
671        }
672        copy.put(KEY_BASE_VERSION_ID, null);
673        copy.put(KEY_IS_CHECKED_IN, null);
674        if (parentId != null) {
675            // reset version
676            copy.put(KEY_MAJOR_VERSION, null);
677            copy.put(KEY_MINOR_VERSION, null);
678        }
679        return copy.getId();
680    }
681
682    protected static final Pattern dotDigitsPattern = Pattern.compile("(.*)\\.[0-9]+$");
683
684    protected String findFreeName(Document parent, String name) {
685        if (hasChild(parent.getUUID(), name)) {
686            Matcher m = dotDigitsPattern.matcher(name);
687            if (m.matches()) {
688                // remove trailing dot and digits
689                name = m.group(1);
690            }
691            // add dot + unique digits
692            name += "." + System.currentTimeMillis();
693        }
694        return name;
695    }
696
697    /** Checks that we don't move/copy under ourselves. */
698    protected void checkNotUnder(String parentId, String id, String op) {
699        // TODO use ancestors
700        String pid = parentId;
701        do {
702            if (pid.equals(id)) {
703                throw new DocumentExistsException("Cannot " + op + " a node under itself: " + parentId + " is under " + id);
704            }
705            State state = transaction.getStateForRead(pid);
706            if (state == null) {
707                // cannot happen
708                throw new NuxeoException("No parent: " + pid);
709            }
710            pid = (String) state.get(KEY_PARENT_ID);
711        } while (pid != null);
712    }
713
714    @Override
715    public Document move(Document source, Document parent, String name) {
716        String oldName = (String) source.getName();
717        if (name == null) {
718            name = oldName;
719        }
720        String sourceId = source.getUUID();
721        String parentId = parent.getUUID();
722        DBSDocumentState sourceState = transaction.getStateForUpdate(sourceId);
723        String oldParentId = (String) sourceState.get(KEY_PARENT_ID);
724
725        // simple case of a rename
726        if (ObjectUtils.equals(oldParentId, parentId)) {
727            if (!oldName.equals(name)) {
728                if (hasChild(parentId, name)) {
729                    throw new DocumentExistsException("Destination name already exists: " + name);
730                }
731                // do the move
732                sourceState.put(KEY_NAME, name);
733                // no ancestors to change
734            }
735            return source;
736        } else {
737            // if not just a simple rename, flush
738            transaction.save();
739            if (hasChild(parentId, name)) {
740                throw new DocumentExistsException("Destination name already exists: " + name);
741            }
742        }
743
744        // prepare new ancestor ids
745        State parentState = transaction.getStateForRead(parentId);
746        Object[] parentAncestorIds = (Object[]) parentState.get(KEY_ANCESTOR_IDS);
747        List<String> ancestorIdsList = new ArrayList<String>();
748        if (parentAncestorIds != null) {
749            for (Object id : parentAncestorIds) {
750                ancestorIdsList.add((String) id);
751            }
752        }
753        ancestorIdsList.add(parentId);
754        Object[] ancestorIds = ancestorIdsList.toArray(new Object[ancestorIdsList.size()]);
755
756        if (ancestorIdsList.contains(sourceId)) {
757            throw new DocumentExistsException("Cannot move a node under itself: " + parentId + " is under " + sourceId);
758        }
759
760        // do the move
761        sourceState.put(KEY_NAME, name);
762        sourceState.put(KEY_PARENT_ID, parentId);
763
764        // update ancestors on all sub-children
765        Object[] oldAncestorIds = (Object[]) sourceState.get(KEY_ANCESTOR_IDS);
766        int ndel = oldAncestorIds == null ? 0 : oldAncestorIds.length;
767        transaction.updateAncestors(sourceId, ndel, ancestorIds);
768
769        // update read acls
770        transaction.updateReadAcls(sourceId);
771
772        return source;
773    }
774
775    /**
776     * Removes a document.
777     * <p>
778     * We also have to update everything impacted by "relations":
779     * <ul>
780     * <li>parent-child relations: delete all subchildren recursively,
781     * <li>proxy-target relations: if a proxy is removed, update the target's PROXY_IDS; and if a target is removed,
782     * raise an error if a proxy still exists for that target.
783     * </ul>
784     */
785    protected void remove(String id) {
786        transaction.save();
787
788        State state = transaction.getStateForRead(id);
789        String versionSeriesId;
790        if (TRUE.equals(state.get(KEY_IS_VERSION))) {
791            versionSeriesId = (String) state.get(KEY_VERSION_SERIES_ID);
792        } else {
793            versionSeriesId = null;
794        }
795        // find all sub-docs and whether they're proxies
796        Map<String, String> proxyTargets = new HashMap<>();
797        Map<String, Object[]> targetProxies = new HashMap<>();
798        Set<String> removedIds = transaction.getSubTree(id, proxyTargets, targetProxies);
799
800        // add this node
801        removedIds.add(id);
802        if (TRUE.equals(state.get(KEY_IS_PROXY))) {
803            String targetId = (String) state.get(KEY_PROXY_TARGET_ID);
804            proxyTargets.put(id, targetId);
805        }
806        Object[] proxyIds = (Object[]) state.get(KEY_PROXY_IDS);
807        if (proxyIds != null) {
808            targetProxies.put(id, proxyIds);
809        }
810
811        // if a proxy target is removed, check that all proxies to it
812        // are removed
813        for (Entry<String, Object[]> en : targetProxies.entrySet()) {
814            String targetId = en.getKey();
815            if (!removedIds.contains(targetId)) {
816                continue;
817            }
818            for (Object proxyId : en.getValue()) {
819                if (!removedIds.contains(proxyId)) {
820                    throw new DocumentExistsException("Cannot remove " + id + ", subdocument " + targetId
821                            + " is the target of proxy " + proxyId);
822                }
823            }
824        }
825
826        // remove all docs
827        transaction.removeStates(removedIds);
828
829        // fix proxies back-pointers on proxy targets
830        Set<String> targetIds = new HashSet<>(proxyTargets.values());
831        for (String targetId : targetIds) {
832            if (removedIds.contains(targetId)) {
833                // the target was also removed, skip
834                continue;
835            }
836            DBSDocumentState target = transaction.getStateForUpdate(targetId);
837            if (target != null) {
838                removeBackProxyIds(target, removedIds);
839            }
840        }
841
842        // recompute version series if needed
843        // only done for root of deletion as versions are not fileable
844        if (versionSeriesId != null) {
845            recomputeVersionSeries(versionSeriesId);
846        }
847    }
848
849    @Override
850    public Document createProxy(Document doc, Document folder) {
851        if (doc == null) {
852            throw new NullPointerException();
853        }
854        String id = doc.getUUID();
855        String targetId;
856        String versionSeriesId;
857        if (doc.isVersion()) {
858            targetId = id;
859            versionSeriesId = doc.getVersionSeriesId();
860        } else if (doc.isProxy()) {
861            // copy the proxy
862            State state = transaction.getStateForRead(id);
863            targetId = (String) state.get(KEY_PROXY_TARGET_ID);
864            versionSeriesId = (String) state.get(KEY_PROXY_VERSION_SERIES_ID);
865        } else {
866            // working copy (live document)
867            targetId = id;
868            versionSeriesId = targetId;
869        }
870
871        String parentId = folder.getUUID();
872        String name = findFreeName(folder, doc.getName());
873        Long pos = parentId == null ? null : getNextPos(parentId);
874
875        DBSDocumentState docState = addProxyState(null, parentId, name, pos, targetId, versionSeriesId);
876        return getDocument(docState);
877    }
878
879    protected DBSDocumentState addProxyState(String id, String parentId, String name, Long pos, String targetId,
880            String versionSeriesId) {
881        DBSDocumentState target = transaction.getStateForUpdate(targetId);
882        String typeName = (String) target.get(KEY_PRIMARY_TYPE);
883
884        DBSDocumentState proxy = transaction.createChild(id, parentId, name, pos, typeName);
885        String proxyId = proxy.getId();
886        proxy.put(KEY_IS_PROXY, TRUE);
887        proxy.put(KEY_PROXY_TARGET_ID, targetId);
888        proxy.put(KEY_PROXY_VERSION_SERIES_ID, versionSeriesId);
889
890        // copy target state to proxy
891        transaction.updateProxy(target, proxyId);
892
893        // add back-reference to proxy on target
894        addBackProxyId(target, proxyId);
895
896        return transaction.getStateForUpdate(proxyId);
897    }
898
899    protected void addBackProxyId(DBSDocumentState docState, String id) {
900        Object[] proxyIds = (Object[]) docState.get(KEY_PROXY_IDS);
901        Object[] newProxyIds;
902        if (proxyIds == null) {
903            newProxyIds = new Object[] { id };
904        } else {
905            newProxyIds = new Object[proxyIds.length + 1];
906            System.arraycopy(proxyIds, 0, newProxyIds, 0, proxyIds.length);
907            newProxyIds[proxyIds.length] = id;
908        }
909        docState.put(KEY_PROXY_IDS, newProxyIds);
910    }
911
912    protected void removeBackProxyId(DBSDocumentState docState, String id) {
913        removeBackProxyIds(docState, Collections.singleton(id));
914    }
915
916    protected void removeBackProxyIds(DBSDocumentState docState, Set<String> ids) {
917        Object[] proxyIds = (Object[]) docState.get(KEY_PROXY_IDS);
918        if (proxyIds == null) {
919            return;
920        }
921        List<Object> keepIds = new ArrayList<>(proxyIds.length);
922        for (Object pid : proxyIds) {
923            if (!ids.contains(pid)) {
924                keepIds.add(pid);
925            }
926        }
927        Object[] newProxyIds = keepIds.isEmpty() ? null : keepIds.toArray(new Object[keepIds.size()]);
928        docState.put(KEY_PROXY_IDS, newProxyIds);
929    }
930
931    @Override
932    public List<Document> getProxies(Document doc, Document folder) {
933        List<DBSDocumentState> docStates;
934        String docId = doc.getUUID();
935        if (doc.isVersion()) {
936            docStates = transaction.getKeyValuedStates(KEY_PROXY_TARGET_ID, docId);
937        } else {
938            String versionSeriesId;
939            if (doc.isProxy()) {
940                State state = transaction.getStateForRead(docId);
941                versionSeriesId = (String) state.get(KEY_PROXY_VERSION_SERIES_ID);
942            } else {
943                versionSeriesId = docId;
944            }
945            docStates = transaction.getKeyValuedStates(KEY_PROXY_VERSION_SERIES_ID, versionSeriesId);
946        }
947
948        String parentId = folder == null ? null : folder.getUUID();
949        List<Document> documents = new ArrayList<Document>(docStates.size());
950        for (DBSDocumentState docState : docStates) {
951            // filter by parent
952            if (parentId != null && !parentId.equals(docState.getParentId())) {
953                continue;
954            }
955            documents.add(getDocument(docState));
956        }
957        return documents;
958    }
959
960    @Override
961    public void setProxyTarget(Document proxy, Document target) {
962        String proxyId = proxy.getUUID();
963        String targetId = target.getUUID();
964        DBSDocumentState proxyState = transaction.getStateForUpdate(proxyId);
965        String oldTargetId = (String) proxyState.get(KEY_PROXY_TARGET_ID);
966
967        // update old target's back-pointers: remove proxy id
968        DBSDocumentState oldTargetState = transaction.getStateForUpdate(oldTargetId);
969        removeBackProxyId(oldTargetState, proxyId);
970        // update new target's back-pointers: add proxy id
971        DBSDocumentState targetState = transaction.getStateForUpdate(targetId);
972        addBackProxyId(targetState, proxyId);
973        // set new target
974        proxyState.put(KEY_PROXY_TARGET_ID, targetId);
975    }
976
977    @Override
978    public Document importDocument(String id, Document parent, String name, String typeName,
979            Map<String, Serializable> properties) {
980        String parentId = parent == null ? null : parent.getUUID();
981        boolean isProxy = typeName.equals(CoreSession.IMPORT_PROXY_TYPE);
982        Map<String, Serializable> props = new HashMap<String, Serializable>();
983        Long pos = null; // TODO pos
984        DBSDocumentState docState;
985        if (isProxy) {
986            // check that target exists and find its typeName
987            String targetId = (String) properties.get(CoreSession.IMPORT_PROXY_TARGET_ID);
988            if (targetId == null) {
989                throw new NuxeoException("Cannot import proxy " + id + " with null target");
990            }
991            State targetState = transaction.getStateForRead(targetId);
992            if (targetState == null) {
993                throw new DocumentNotFoundException("Cannot import proxy " + id + " with missing target " + targetId);
994            }
995            String versionSeriesId = (String) properties.get(CoreSession.IMPORT_PROXY_VERSIONABLE_ID);
996            docState = addProxyState(id, parentId, name, pos, targetId, versionSeriesId);
997        } else {
998            // version & live document
999            props.put(KEY_LIFECYCLE_POLICY, properties.get(CoreSession.IMPORT_LIFECYCLE_POLICY));
1000            props.put(KEY_LIFECYCLE_STATE, properties.get(CoreSession.IMPORT_LIFECYCLE_STATE));
1001            // compat with old lock import
1002            @SuppressWarnings("deprecation")
1003            String key = (String) properties.get(CoreSession.IMPORT_LOCK);
1004            if (key != null) {
1005                String[] values = key.split(":");
1006                if (values.length == 2) {
1007                    String owner = values[0];
1008                    Calendar created = new GregorianCalendar();
1009                    try {
1010                        created.setTimeInMillis(DateFormat.getDateInstance(DateFormat.MEDIUM).parse(values[1]).getTime());
1011                    } catch (ParseException e) {
1012                        // use current date
1013                    }
1014                    props.put(KEY_LOCK_OWNER, owner);
1015                    props.put(KEY_LOCK_CREATED, created);
1016                }
1017            }
1018
1019            Serializable importLockOwnerProp = properties.get(CoreSession.IMPORT_LOCK_OWNER);
1020            if (importLockOwnerProp != null) {
1021                props.put(KEY_LOCK_OWNER, importLockOwnerProp);
1022            }
1023            Serializable importLockCreatedProp = properties.get(CoreSession.IMPORT_LOCK_CREATED);
1024            if (importLockCreatedProp != null) {
1025                props.put(KEY_LOCK_CREATED, importLockCreatedProp);
1026            }
1027
1028            props.put(KEY_MAJOR_VERSION, properties.get(CoreSession.IMPORT_VERSION_MAJOR));
1029            props.put(KEY_MINOR_VERSION, properties.get(CoreSession.IMPORT_VERSION_MINOR));
1030            Boolean isVersion = trueOrNull(properties.get(CoreSession.IMPORT_IS_VERSION));
1031            props.put(KEY_IS_VERSION, isVersion);
1032            if (TRUE.equals(isVersion)) {
1033                // version
1034                props.put(KEY_VERSION_SERIES_ID, properties.get(CoreSession.IMPORT_VERSION_VERSIONABLE_ID));
1035                props.put(KEY_VERSION_CREATED, properties.get(CoreSession.IMPORT_VERSION_CREATED));
1036                props.put(KEY_VERSION_LABEL, properties.get(CoreSession.IMPORT_VERSION_LABEL));
1037                props.put(KEY_VERSION_DESCRIPTION, properties.get(CoreSession.IMPORT_VERSION_DESCRIPTION));
1038                // TODO maybe these should be recomputed at end of import:
1039                props.put(KEY_IS_LATEST_VERSION, trueOrNull(properties.get(CoreSession.IMPORT_VERSION_IS_LATEST)));
1040                props.put(KEY_IS_LATEST_MAJOR_VERSION,
1041                        trueOrNull(properties.get(CoreSession.IMPORT_VERSION_IS_LATEST_MAJOR)));
1042            } else {
1043                // live document
1044                props.put(KEY_BASE_VERSION_ID, properties.get(CoreSession.IMPORT_BASE_VERSION_ID));
1045                props.put(KEY_IS_CHECKED_IN, trueOrNull(properties.get(CoreSession.IMPORT_CHECKED_IN)));
1046            }
1047            docState = createChildState(id, parentId, name, pos, typeName);
1048        }
1049        for (Entry<String, Serializable> entry : props.entrySet()) {
1050            docState.put(entry.getKey(), entry.getValue());
1051        }
1052        return getDocument(docState, false); // not readonly
1053    }
1054
1055    protected static Boolean trueOrNull(Object value) {
1056        return TRUE.equals(value) ? TRUE : null;
1057    }
1058
1059    @Override
1060    public Document getVersion(String versionSeriesId, VersionModel versionModel) {
1061        DBSDocumentState docState = getVersionByLabel(versionSeriesId, versionModel.getLabel());
1062        if (docState == null) {
1063            return null;
1064        }
1065        versionModel.setDescription((String) docState.get(KEY_VERSION_DESCRIPTION));
1066        versionModel.setCreated((Calendar) docState.get(KEY_VERSION_CREATED));
1067        return getDocument(docState);
1068    }
1069
1070    protected DBSDocumentState getVersionByLabel(String versionSeriesId, String label) {
1071        List<DBSDocumentState> docStates = transaction.getKeyValuedStates(KEY_VERSION_SERIES_ID, versionSeriesId,
1072                KEY_IS_VERSION, TRUE);
1073        for (DBSDocumentState docState : docStates) {
1074            if (label.equals(docState.get(KEY_VERSION_LABEL))) {
1075                return docState;
1076            }
1077        }
1078        return null;
1079    }
1080
1081    protected List<String> getVersionsIds(String versionSeriesId) {
1082        // order by creation date
1083        List<DBSDocumentState> docStates = transaction.getKeyValuedStates(KEY_VERSION_SERIES_ID, versionSeriesId,
1084                KEY_IS_VERSION, TRUE);
1085        Collections.sort(docStates, VERSION_CREATED_COMPARATOR);
1086        List<String> ids = new ArrayList<String>(docStates.size());
1087        for (DBSDocumentState docState : docStates) {
1088            ids.add(docState.getId());
1089        }
1090        return ids;
1091    }
1092
1093    protected Document getLastVersion(String versionSeriesId) {
1094        List<DBSDocumentState> docStates = transaction.getKeyValuedStates(KEY_VERSION_SERIES_ID, versionSeriesId,
1095                KEY_IS_VERSION, TRUE);
1096        // find latest one
1097        Calendar latest = null;
1098        DBSDocumentState latestState = null;
1099        for (DBSDocumentState docState : docStates) {
1100            Calendar created = (Calendar) docState.get(KEY_VERSION_CREATED);
1101            if (latest == null || created.compareTo(latest) > 0) {
1102                latest = created;
1103                latestState = docState;
1104            }
1105        }
1106        return latestState == null ? null : getDocument(latestState);
1107    }
1108
1109    private static final Comparator<DBSDocumentState> VERSION_CREATED_COMPARATOR = new Comparator<DBSDocumentState>() {
1110        @Override
1111        public int compare(DBSDocumentState s1, DBSDocumentState s2) {
1112            Calendar c1 = (Calendar) s1.get(KEY_VERSION_CREATED);
1113            Calendar c2 = (Calendar) s2.get(KEY_VERSION_CREATED);
1114            if (c1 == null && c2 == null) {
1115                // coherent sort
1116                return s1.hashCode() - s2.hashCode();
1117            }
1118            if (c1 == null) {
1119                return 1;
1120            }
1121            if (c2 == null) {
1122                return -1;
1123            }
1124            return c1.compareTo(c2);
1125        }
1126    };
1127
1128    private static final Comparator<DBSDocumentState> POS_COMPARATOR = new Comparator<DBSDocumentState>() {
1129        @Override
1130        public int compare(DBSDocumentState s1, DBSDocumentState s2) {
1131            Long p1 = (Long) s1.get(KEY_POS);
1132            Long p2 = (Long) s2.get(KEY_POS);
1133            if (p1 == null && p2 == null) {
1134                // coherent sort
1135                return s1.hashCode() - s2.hashCode();
1136            }
1137            if (p1 == null) {
1138                return 1;
1139            }
1140            if (p2 == null) {
1141                return -1;
1142            }
1143            return p1.compareTo(p2);
1144        }
1145    };
1146
1147    @Override
1148    public boolean isNegativeAclAllowed() {
1149        return false;
1150    }
1151
1152    // TODO move logic higher
1153    @Override
1154    public ACP getMergedACP(Document doc) {
1155        Document base = doc.isVersion() ? doc.getSourceDocument() : doc;
1156        if (base == null) {
1157            return null;
1158        }
1159        ACP acp = getACP(base);
1160        if (doc.getParent() == null) {
1161            return acp;
1162        }
1163        // get inherited ACLs only if no blocking inheritance ACE exists
1164        // in the top level ACP.
1165        ACL acl = null;
1166        if (acp == null || acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) != Access.DENY) {
1167            acl = getInheritedACLs(doc);
1168        }
1169        if (acp == null) {
1170            if (acl == null) {
1171                return null;
1172            }
1173            acp = new ACPImpl();
1174        }
1175        if (acl != null) {
1176            acp.addACL(acl);
1177        }
1178        return acp;
1179    }
1180
1181    protected ACL getInheritedACLs(Document doc) {
1182        doc = doc.getParent();
1183        ACL merged = null;
1184        while (doc != null) {
1185            ACP acp = getACP(doc);
1186            if (acp != null) {
1187                ACL acl = acp.getMergedACLs(ACL.INHERITED_ACL);
1188                if (merged == null) {
1189                    merged = acl;
1190                } else {
1191                    merged.addAll(acl);
1192                }
1193                if (acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) == Access.DENY) {
1194                    break;
1195                }
1196            }
1197            doc = doc.getParent();
1198        }
1199        return merged;
1200    }
1201
1202    protected ACP getACP(Document doc) {
1203        State state = transaction.getStateForRead(doc.getUUID());
1204        return memToAcp(state.get(KEY_ACP));
1205    }
1206
1207    @Override
1208    public void setACP(Document doc, ACP acp, boolean overwrite) {
1209        checkNegativeAcl(acp);
1210        if (!overwrite) {
1211            if (acp == null) {
1212                return;
1213            }
1214            // merge with existing
1215            acp = updateACP(getACP(doc), acp);
1216        }
1217        String id = doc.getUUID();
1218        DBSDocumentState docState = transaction.getStateForUpdate(id);
1219        docState.put(KEY_ACP, acpToMem(acp));
1220        transaction.save(); // read acls update needs full tree
1221        transaction.updateReadAcls(id);
1222    }
1223
1224    protected void checkNegativeAcl(ACP acp) {
1225        if (acp == null) {
1226            return;
1227        }
1228        for (ACL acl : acp.getACLs()) {
1229            if (acl.getName().equals(ACL.INHERITED_ACL)) {
1230                continue;
1231            }
1232            for (ACE ace : acl.getACEs()) {
1233                if (ace.isGranted()) {
1234                    continue;
1235                }
1236                String permission = ace.getPermission();
1237                if (permission.equals(SecurityConstants.EVERYTHING)
1238                        && ace.getUsername().equals(SecurityConstants.EVERYONE)) {
1239                    continue;
1240                }
1241                // allow Write, as we're sure it doesn't include Read/Browse
1242                if (permission.equals(SecurityConstants.WRITE)) {
1243                    continue;
1244                }
1245                throw new IllegalArgumentException("Negative ACL not allowed: " + ace);
1246            }
1247        }
1248    }
1249
1250    /**
1251     * Returns the merge of two ACPs.
1252     */
1253    // TODO move to ACPImpl
1254    protected static ACP updateACP(ACP curAcp, ACP addAcp) {
1255        if (curAcp == null) {
1256            return addAcp;
1257        }
1258        ACP newAcp = curAcp.clone();
1259        Map<String, ACL> acls = new HashMap<String, ACL>();
1260        for (ACL acl : newAcp.getACLs()) {
1261            String name = acl.getName();
1262            if (ACL.INHERITED_ACL.equals(name)) {
1263                throw new IllegalStateException(curAcp.toString());
1264            }
1265            acls.put(name, acl);
1266        }
1267        for (ACL acl : addAcp.getACLs()) {
1268            String name = acl.getName();
1269            if (ACL.INHERITED_ACL.equals(name)) {
1270                continue;
1271            }
1272            ACL curAcl = acls.get(name);
1273            if (curAcl != null) {
1274                // TODO avoid duplicates
1275                curAcl.addAll(acl);
1276            } else {
1277                newAcp.addACL(acl);
1278            }
1279        }
1280        return newAcp;
1281    }
1282
1283    protected static Serializable acpToMem(ACP acp) {
1284        if (acp == null) {
1285            return null;
1286        }
1287        ACL[] acls = acp.getACLs();
1288        if (acls.length == 0) {
1289            return null;
1290        }
1291        List<Serializable> aclList = new ArrayList<Serializable>(acls.length);
1292        for (ACL acl : acls) {
1293            String name = acl.getName();
1294            if (name.equals(ACL.INHERITED_ACL)) {
1295                continue;
1296            }
1297            ACE[] aces = acl.getACEs();
1298            List<Serializable> aceList = new ArrayList<Serializable>(aces.length);
1299            for (ACE ace : aces) {
1300                State aceMap = new State(6);
1301                aceMap.put(KEY_ACE_USER, ace.getUsername());
1302                aceMap.put(KEY_ACE_PERMISSION, ace.getPermission());
1303                aceMap.put(KEY_ACE_GRANT, Boolean.valueOf(ace.isGranted()));
1304                String creator = ace.getCreator();
1305                if (creator != null) {
1306                    aceMap.put(KEY_ACE_CREATOR, creator);
1307                }
1308                Calendar begin = ace.getBegin();
1309                if (begin != null) {
1310                    aceMap.put(KEY_ACE_BEGIN, begin);
1311                }
1312                Calendar end = ace.getEnd();
1313                if (end != null) {
1314                    aceMap.put(KEY_ACE_END, end);
1315                }
1316                Long status = ace.getLongStatus();
1317                if (status != null) {
1318                    aceMap.put(KEY_ACE_STATUS, status);
1319                }
1320                aceList.add(aceMap);
1321            }
1322            if (aceList.isEmpty()) {
1323                continue;
1324            }
1325            State aclMap = new State(2);
1326            aclMap.put(KEY_ACL_NAME, name);
1327            aclMap.put(KEY_ACL, (Serializable) aceList);
1328            aclList.add(aclMap);
1329        }
1330        return (Serializable) aclList;
1331    }
1332
1333    protected static ACP memToAcp(Serializable acpSer) {
1334        if (acpSer == null) {
1335            return null;
1336        }
1337        @SuppressWarnings("unchecked")
1338        List<Serializable> aclList = (List<Serializable>) acpSer;
1339        ACP acp = new ACPImpl();
1340        for (Serializable aclSer : aclList) {
1341            State aclMap = (State) aclSer;
1342            String name = (String) aclMap.get(KEY_ACL_NAME);
1343            @SuppressWarnings("unchecked")
1344            List<Serializable> aceList = (List<Serializable>) aclMap.get(KEY_ACL);
1345            if (aceList == null) {
1346                continue;
1347            }
1348            ACL acl = new ACLImpl(name);
1349            for (Serializable aceSer : aceList) {
1350                State aceMap = (State) aceSer;
1351                String username = (String) aceMap.get(KEY_ACE_USER);
1352                String permission = (String) aceMap.get(KEY_ACE_PERMISSION);
1353                Boolean granted = (Boolean) aceMap.get(KEY_ACE_GRANT);
1354                String creator = (String) aceMap.get(KEY_ACE_CREATOR);
1355                Calendar begin = (Calendar) aceMap.get(KEY_ACE_BEGIN);
1356                Calendar end = (Calendar) aceMap.get(KEY_ACE_END);
1357                // status not read, ACE always computes it on read
1358                ACE ace = ACE.builder(username, permission).isGranted(granted.booleanValue()).creator(creator).begin(
1359                        begin).end(end).build();
1360                acl.add(ace);
1361            }
1362            acp.addACL(acl);
1363        }
1364        return acp;
1365    }
1366
1367    @Override
1368    public Map<String, String> getBinaryFulltext(String id) {
1369        State state = transaction.getStateForRead(id);
1370        String fulltext = (String) state.get(KEY_FULLTEXT_BINARY);
1371        return Collections.singletonMap("binarytext", fulltext);
1372    }
1373
1374    @Override
1375    public PartialList<Document> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) {
1376        // query
1377        PartialList<String> pl = doQuery(query, queryType, queryFilter, (int) countUpTo);
1378
1379        // get Documents in bulk
1380        List<Document> docs = getDocuments(pl.list);
1381
1382        return new PartialList<>(docs, pl.totalSize);
1383    }
1384
1385    protected PartialList<String> doQuery(String query, String queryType, QueryFilter queryFilter, int countUpTo) {
1386        PartialList<Map<String, Serializable>> pl = doQueryAndFetch(query, queryType, queryFilter, countUpTo);
1387        List<String> ids = new ArrayList<String>(pl.list.size());
1388        for (Map<String, Serializable> map : pl.list) {
1389            String id = (String) map.get(NXQL.ECM_UUID);
1390            ids.add(id);
1391        }
1392        return new PartialList<String>(ids, pl.totalSize);
1393    }
1394
1395    protected PartialList<Map<String, Serializable>> doQueryAndFetch(String query, String queryType,
1396            QueryFilter queryFilter, int countUpTo) {
1397        if ("NXTAG".equals(queryType)) {
1398            // for now don't try to implement tags
1399            // and return an empty list
1400            return new PartialList<Map<String, Serializable>>(Collections.<Map<String, Serializable>> emptyList(), 0);
1401        }
1402        if (!NXQL.NXQL.equals(queryType)) {
1403            throw new NuxeoException("No QueryMaker accepts query type: " + queryType);
1404        }
1405        // transform the query according to the transformers defined by the
1406        // security policies
1407        SQLQuery sqlQuery = SQLQueryParser.parse(query);
1408        SelectClause selectClause = sqlQuery.select;
1409        if (selectClause.isEmpty()) {
1410            // turned into SELECT ecm:uuid
1411            selectClause.add(new Reference(NXQL.ECM_UUID));
1412        }
1413        if (selectClause.isDistinct()) {
1414            if (selectClause.getSelectList().size() == 1
1415                    && (selectClause.get(0).equals(new Reference(NXQL.ECM_UUID)))) {
1416                // ok, SELECT ecm:uuid
1417            } else {
1418                throw new QueryParseException("SELECT DISTINCT not supported on DBS");
1419            }
1420        }
1421        for (SQLQuery.Transformer transformer : queryFilter.getQueryTransformers()) {
1422            sqlQuery = transformer.transform(queryFilter.getPrincipal(), sqlQuery);
1423        }
1424        OrderByClause orderByClause = sqlQuery.orderBy;
1425
1426        QueryOptimizer optimizer = new QueryOptimizer();
1427        MultiExpression expression = optimizer.getOptimizedQuery(sqlQuery, queryFilter.getFacetFilter());
1428        boolean fulltextDisabled = repository.getFulltextConfiguration() == null;
1429        DBSExpressionEvaluator evaluator = new DBSExpressionEvaluator(this, selectClause, expression, orderByClause,
1430                queryFilter.getPrincipals(), fulltextDisabled);
1431
1432        int limit = (int) queryFilter.getLimit();
1433        int offset = (int) queryFilter.getOffset();
1434        if (offset < 0) {
1435            offset = 0;
1436        }
1437        if (limit < 0) {
1438            limit = 0;
1439        }
1440
1441        int repoLimit;
1442        int repoOffset;
1443        OrderByClause repoOrderByClause;
1444        boolean postFilter = isOrderByPath(orderByClause);
1445        if (postFilter) {
1446            // we have to merge ordering and batching between memory and
1447            // repository
1448            repoLimit = 0;
1449            repoOffset = 0;
1450            repoOrderByClause = null;
1451        } else {
1452            // fast case, we can use the repository query directly
1453            repoLimit = limit;
1454            repoOffset = offset;
1455            repoOrderByClause = orderByClause;
1456        }
1457
1458        // query the repository
1459        PartialList<Map<String, Serializable>> pl = repository.queryAndFetch(evaluator, repoOrderByClause, repoLimit,
1460                repoOffset, countUpTo);
1461
1462        List<Map<String, Serializable>> projections = pl.list;
1463        long totalSize = pl.totalSize;
1464        if (totalSize >= 0) {
1465            if (countUpTo == -1) {
1466                // count full size
1467            } else if (countUpTo == 0) {
1468                // no count
1469                totalSize = -1; // not counted
1470            } else {
1471                // count only if less than countUpTo
1472                if (totalSize > countUpTo) {
1473                    totalSize = -2; // truncated
1474                }
1475            }
1476        }
1477
1478        if (postFilter) {
1479            // ORDER BY
1480            if (orderByClause != null) {
1481                doOrderBy(projections, orderByClause);
1482            }
1483            // LIMIT / OFFSET
1484            if (limit != 0) {
1485                int size = projections.size();
1486                projections.subList(0, offset > size ? size : offset).clear();
1487                size = projections.size();
1488                if (limit < size) {
1489                    projections.subList(limit, size).clear();
1490                }
1491            }
1492        }
1493
1494        return new PartialList<Map<String, Serializable>>(projections, totalSize);
1495    }
1496
1497    /** Does an ORDER BY clause include ecm:path */
1498    protected boolean isOrderByPath(OrderByClause orderByClause) {
1499        if (orderByClause == null) {
1500            return false;
1501        }
1502        for (OrderByExpr ob : orderByClause.elements) {
1503            if (ob.reference.name.equals(NXQL.ECM_PATH)) {
1504                return true;
1505            }
1506        }
1507        return false;
1508    }
1509
1510    protected String getPath(Map<String, Serializable> projection) {
1511        String name = (String) projection.get(NXQL.ECM_NAME);
1512        String parentId = (String) projection.get(NXQL.ECM_PARENTID);
1513        State state;
1514        if (parentId == null || (state = transaction.getStateForRead(parentId)) == null) {
1515            if ("".equals(name)) {
1516                return "/"; // root
1517            } else {
1518                return name; // placeless, no slash
1519            }
1520        }
1521        LinkedList<String> list = new LinkedList<String>();
1522        list.addFirst(name);
1523        for (;;) {
1524            name = (String) state.get(KEY_NAME);
1525            parentId = (String) state.get(KEY_PARENT_ID);
1526            list.addFirst(name);
1527            if (parentId == null || (state = transaction.getStateForRead(parentId)) == null) {
1528                return StringUtils.join(list, '/');
1529            }
1530        }
1531    }
1532
1533    protected void doOrderBy(List<Map<String, Serializable>> projections, OrderByClause orderByClause) {
1534        if (isOrderByPath(orderByClause)) {
1535            // add path info to do the sort
1536            for (Map<String, Serializable> projection : projections) {
1537                projection.put(ExpressionEvaluator.NXQL_ECM_PATH, getPath(projection));
1538            }
1539        }
1540        Collections.sort(projections, new OrderByComparator(orderByClause));
1541    }
1542
1543    public static class OrderByComparator implements Comparator<Map<String, Serializable>> {
1544
1545        protected final OrderByClause orderByClause;
1546
1547        public OrderByComparator(OrderByClause orderByClause) {
1548            // replace ecm:path with ecm:__path for evaluation
1549            // (we don't want to allow ecm:path to be usable anywhere else
1550            // and resolve to a null value)
1551            OrderByList obl = new OrderByList(null); // stupid constructor
1552            obl.clear();
1553            for (OrderByExpr ob : orderByClause.elements) {
1554                if (ob.reference.name.equals(NXQL.ECM_PATH)) {
1555                    ob = new OrderByExpr(new Reference(ExpressionEvaluator.NXQL_ECM_PATH), ob.isDescending);
1556                }
1557                obl.add(ob);
1558            }
1559            this.orderByClause = new OrderByClause(obl);
1560        }
1561
1562        @Override
1563        public int compare(Map<String, Serializable> map1, Map<String, Serializable> map2) {
1564            for (OrderByExpr ob : orderByClause.elements) {
1565                Reference ref = ob.reference;
1566                boolean desc = ob.isDescending;
1567                Object v1 = map1.get(ref.name);
1568                Object v2 = map2.get(ref.name);
1569                int cmp;
1570                if (v1 == null) {
1571                    cmp = v2 == null ? 0 : -1;
1572                } else if (v2 == null) {
1573                    cmp = 1;
1574                } else {
1575                    if (!(v1 instanceof Comparable)) {
1576                        throw new QueryParseException("Not a comparable: " + v1);
1577                    }
1578                    cmp = ((Comparable<Object>) v1).compareTo(v2);
1579                }
1580                if (desc) {
1581                    cmp = -cmp;
1582                }
1583                if (cmp != 0) {
1584                    return cmp;
1585                }
1586                // loop for lexicographical comparison
1587            }
1588            return 0;
1589        }
1590    }
1591
1592    @Override
1593    public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, Object[] params) {
1594        int countUpTo = -1;
1595        PartialList<Map<String, Serializable>> pl = doQueryAndFetch(query, queryType, queryFilter, countUpTo);
1596        return new DBSQueryResult(pl);
1597    }
1598
1599    protected static class DBSQueryResult implements IterableQueryResult, Iterator<Map<String, Serializable>> {
1600
1601        boolean closed;
1602
1603        protected List<Map<String, Serializable>> maps;
1604
1605        protected long totalSize;
1606
1607        protected long pos;
1608
1609        protected DBSQueryResult(PartialList<Map<String, Serializable>> pl) {
1610            this.maps = pl.list;
1611            this.totalSize = pl.totalSize;
1612        }
1613
1614        @Override
1615        public Iterator<Map<String, Serializable>> iterator() {
1616            return this;
1617        }
1618
1619        @Override
1620        public void close() {
1621            closed = true;
1622            pos = -1;
1623        }
1624
1625        @Override
1626        public boolean isLife() {
1627            return !closed;
1628        }
1629
1630        @Override
1631        public boolean mustBeClosed() {
1632            return false; // holds no resources
1633        }
1634
1635        @Override
1636        public long size() {
1637            return totalSize;
1638        }
1639
1640        @Override
1641        public long pos() {
1642            return pos;
1643        }
1644
1645        @Override
1646        public void skipTo(long pos) {
1647            if (pos < 0) {
1648                pos = 0;
1649            } else if (pos > totalSize) {
1650                pos = totalSize;
1651            }
1652            this.pos = pos;
1653        }
1654
1655        @Override
1656        public boolean hasNext() {
1657            return pos < totalSize;
1658        }
1659
1660        @Override
1661        public Map<String, Serializable> next() {
1662            if (closed || pos == totalSize) {
1663                throw new NoSuchElementException();
1664            }
1665            Map<String, Serializable> map = maps.get((int) pos);
1666            pos++;
1667            return map;
1668        }
1669
1670        @Override
1671        public void remove() {
1672            throw new UnsupportedOperationException();
1673        }
1674    }
1675
1676    public static String convToInternal(String name) {
1677        switch (name) {
1678        case NXQL.ECM_UUID:
1679            return KEY_ID;
1680        case NXQL.ECM_NAME:
1681            return KEY_NAME;
1682        case NXQL.ECM_POS:
1683            return KEY_POS;
1684        case NXQL.ECM_PARENTID:
1685            return KEY_PARENT_ID;
1686        case NXQL.ECM_MIXINTYPE:
1687            return KEY_MIXIN_TYPES;
1688        case NXQL.ECM_PRIMARYTYPE:
1689            return KEY_PRIMARY_TYPE;
1690        case NXQL.ECM_ISPROXY:
1691            return KEY_IS_PROXY;
1692        case NXQL.ECM_ISVERSION:
1693        case NXQL.ECM_ISVERSION_OLD:
1694            return KEY_IS_VERSION;
1695        case NXQL.ECM_LIFECYCLESTATE:
1696            return KEY_LIFECYCLE_STATE;
1697        case NXQL.ECM_LOCK_OWNER:
1698            return KEY_LOCK_OWNER;
1699        case NXQL.ECM_LOCK_CREATED:
1700            return KEY_LOCK_CREATED;
1701        case NXQL.ECM_PROXY_TARGETID:
1702            return KEY_PROXY_TARGET_ID;
1703        case NXQL.ECM_PROXY_VERSIONABLEID:
1704            return KEY_PROXY_VERSION_SERIES_ID;
1705        case NXQL.ECM_ISCHECKEDIN:
1706            return KEY_IS_CHECKED_IN;
1707        case NXQL.ECM_ISLATESTVERSION:
1708            return KEY_IS_LATEST_VERSION;
1709        case NXQL.ECM_ISLATESTMAJORVERSION:
1710            return KEY_IS_LATEST_MAJOR_VERSION;
1711        case NXQL.ECM_VERSIONLABEL:
1712            return KEY_VERSION_LABEL;
1713        case NXQL.ECM_VERSIONCREATED:
1714            return KEY_VERSION_CREATED;
1715        case NXQL.ECM_VERSIONDESCRIPTION:
1716            return KEY_VERSION_DESCRIPTION;
1717        case NXQL.ECM_VERSION_VERSIONABLEID:
1718            return KEY_VERSION_SERIES_ID;
1719        case ExpressionEvaluator.NXQL_ECM_ANCESTOR_IDS:
1720            return KEY_ANCESTOR_IDS;
1721        case ExpressionEvaluator.NXQL_ECM_PATH:
1722            return KEY_PATH_INTERNAL;
1723        case ExpressionEvaluator.NXQL_ECM_READ_ACL:
1724            return KEY_READ_ACL;
1725        case NXQL.ECM_FULLTEXT_JOBID:
1726            return KEY_FULLTEXT_JOBID;
1727        case NXQL.ECM_FULLTEXT_SCORE:
1728            return KEY_FULLTEXT_SCORE;
1729        case ExpressionEvaluator.NXQL_ECM_FULLTEXT_SIMPLE:
1730            return KEY_FULLTEXT_SIMPLE;
1731        case ExpressionEvaluator.NXQL_ECM_FULLTEXT_BINARY:
1732            return KEY_FULLTEXT_BINARY;
1733        case NXQL.ECM_ACL:
1734            return KEY_ACP;
1735        case NXQL.ECM_FULLTEXT:
1736        case NXQL.ECM_TAG:
1737            throw new UnsupportedOperationException(name);
1738        }
1739        throw new QueryParseException("No such property: " + name);
1740    }
1741
1742    public static String convToInternalAce(String name) {
1743        switch (name) {
1744        case NXQL.ECM_ACL_NAME:
1745            return KEY_ACL_NAME;
1746        case NXQL.ECM_ACL_PRINCIPAL:
1747            return KEY_ACE_USER;
1748        case NXQL.ECM_ACL_PERMISSION:
1749            return KEY_ACE_PERMISSION;
1750        case NXQL.ECM_ACL_GRANT:
1751            return KEY_ACE_GRANT;
1752        case NXQL.ECM_ACL_CREATOR:
1753            return KEY_ACE_CREATOR;
1754        case NXQL.ECM_ACL_BEGIN:
1755            return KEY_ACE_BEGIN;
1756        case NXQL.ECM_ACL_END:
1757            return KEY_ACE_END;
1758        case NXQL.ECM_ACL_STATUS:
1759            return KEY_ACE_STATUS;
1760        }
1761        return null;
1762    }
1763
1764    public static String convToNXQL(String name) {
1765        switch (name) {
1766        case KEY_ID:
1767            return NXQL.ECM_UUID;
1768        case KEY_NAME:
1769            return NXQL.ECM_NAME;
1770        case KEY_POS:
1771            return NXQL.ECM_POS;
1772        case KEY_PARENT_ID:
1773            return NXQL.ECM_PARENTID;
1774        case KEY_MIXIN_TYPES:
1775            return NXQL.ECM_MIXINTYPE;
1776        case KEY_PRIMARY_TYPE:
1777            return NXQL.ECM_PRIMARYTYPE;
1778        case KEY_IS_PROXY:
1779            return NXQL.ECM_ISPROXY;
1780        case KEY_IS_VERSION:
1781            return NXQL.ECM_ISVERSION;
1782        case KEY_LIFECYCLE_STATE:
1783            return NXQL.ECM_LIFECYCLESTATE;
1784        case KEY_LOCK_OWNER:
1785            return NXQL.ECM_LOCK_OWNER;
1786        case KEY_LOCK_CREATED:
1787            return NXQL.ECM_LOCK_CREATED;
1788        case KEY_PROXY_TARGET_ID:
1789            return NXQL.ECM_PROXY_TARGETID;
1790        case KEY_PROXY_VERSION_SERIES_ID:
1791            return NXQL.ECM_PROXY_VERSIONABLEID;
1792        case KEY_IS_CHECKED_IN:
1793            return NXQL.ECM_ISCHECKEDIN;
1794        case KEY_IS_LATEST_VERSION:
1795            return NXQL.ECM_ISLATESTVERSION;
1796        case KEY_IS_LATEST_MAJOR_VERSION:
1797            return NXQL.ECM_ISLATESTMAJORVERSION;
1798        case KEY_VERSION_LABEL:
1799            return NXQL.ECM_VERSIONLABEL;
1800        case KEY_VERSION_CREATED:
1801            return NXQL.ECM_VERSIONCREATED;
1802        case KEY_VERSION_DESCRIPTION:
1803            return NXQL.ECM_VERSIONDESCRIPTION;
1804        case KEY_VERSION_SERIES_ID:
1805            return NXQL.ECM_VERSION_VERSIONABLEID;
1806        case KEY_MAJOR_VERSION:
1807            return "major_version"; // TODO XXX constant
1808        case KEY_MINOR_VERSION:
1809            return "minor_version";
1810        case KEY_FULLTEXT_SCORE:
1811            return NXQL.ECM_FULLTEXT_SCORE;
1812        case KEY_LIFECYCLE_POLICY:
1813        case KEY_ACP:
1814        case KEY_ANCESTOR_IDS:
1815        case KEY_BASE_VERSION_ID:
1816        case KEY_READ_ACL:
1817        case KEY_FULLTEXT_SIMPLE:
1818        case KEY_FULLTEXT_BINARY:
1819        case KEY_FULLTEXT_JOBID:
1820        case KEY_PATH_INTERNAL:
1821            return null;
1822        }
1823        throw new QueryParseException("No such property: " + name);
1824    }
1825
1826    public static boolean isBoolean(String name) {
1827        return getType(name) instanceof BooleanType;
1828    }
1829
1830    protected static final Type STRING_ARRAY_TYPE = new ListTypeImpl("", "", StringType.INSTANCE);
1831
1832    public static Type getType(String name) {
1833        switch (name) {
1834        case KEY_IS_VERSION:
1835        case KEY_IS_CHECKED_IN:
1836        case KEY_IS_LATEST_VERSION:
1837        case KEY_IS_LATEST_MAJOR_VERSION:
1838        case KEY_IS_PROXY:
1839        case KEY_ACE_GRANT:
1840            return BooleanType.INSTANCE;
1841        case KEY_VERSION_CREATED:
1842        case KEY_LOCK_CREATED:
1843        case KEY_ACE_BEGIN:
1844        case KEY_ACE_END:
1845            return DateType.INSTANCE;
1846        case KEY_MIXIN_TYPES:
1847        case KEY_ANCESTOR_IDS:
1848        case KEY_PROXY_IDS:
1849            return STRING_ARRAY_TYPE;
1850        }
1851        return null;
1852    }
1853
1854    @Override
1855    public LockManager getLockManager() {
1856        return repository.getLockManager();
1857    }
1858
1859}