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