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