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