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