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