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