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