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