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