001/*
002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage.sql.coremodel;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Calendar;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.LinkedList;
031import java.util.List;
032import java.util.Map;
033import java.util.Map.Entry;
034import java.util.Set;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037import java.util.stream.Collectors;
038
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.nuxeo.ecm.core.api.CoreSession;
042import org.nuxeo.ecm.core.api.DocumentNotFoundException;
043import org.nuxeo.ecm.core.api.IterableQueryResult;
044import org.nuxeo.ecm.core.api.PartialList;
045import org.nuxeo.ecm.core.api.ScrollResult;
046import org.nuxeo.ecm.core.api.VersionModel;
047import org.nuxeo.ecm.core.api.lock.LockManager;
048import org.nuxeo.ecm.core.api.repository.FulltextConfiguration;
049import org.nuxeo.ecm.core.api.security.ACE;
050import org.nuxeo.ecm.core.api.security.ACL;
051import org.nuxeo.ecm.core.api.security.ACP;
052import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
053import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
054import org.nuxeo.ecm.core.model.BaseSession;
055import org.nuxeo.ecm.core.model.Document;
056import org.nuxeo.ecm.core.model.Repository;
057import org.nuxeo.ecm.core.query.QueryFilter;
058import org.nuxeo.ecm.core.query.sql.NXQL;
059import org.nuxeo.ecm.core.schema.DocumentType;
060import org.nuxeo.ecm.core.schema.SchemaManager;
061import org.nuxeo.ecm.core.storage.sql.ACLRow;
062import org.nuxeo.ecm.core.storage.sql.Model;
063import org.nuxeo.ecm.core.storage.sql.Node;
064import org.nuxeo.ecm.core.storage.sql.SessionImpl;
065import org.nuxeo.runtime.api.Framework;
066
067/**
068 * This class is the bridge between the Nuxeo SPI Session and the actual low-level implementation of the SQL storage
069 * session.
070 *
071 * @author Florent Guillaume
072 */
073public class SQLSession extends BaseSession {
074
075    protected final Log log = LogFactory.getLog(SQLSession.class);
076
077    /**
078     * Framework property to control whether negative ACLs (deny) are allowed.
079     *
080     * @since 6.0
081     */
082    public static final String ALLOW_NEGATIVE_ACL_PROPERTY = "nuxeo.security.allowNegativeACL";
083
084    /**
085     * Framework property to disabled free-name collision detection for copy. This is useful when constraints have been
086     * added to the database to detect collisions at the database level and raise a ConcurrentUpdateException, thus
087     * letting the high-level application deal with collisions.
088     *
089     * @since 7.3
090     */
091    public static final String COPY_FINDFREENAME_DISABLED_PROP = "nuxeo.vcs.copy.findFreeName.disabled";
092
093    private final SessionImpl session;
094
095    private final boolean negativeAclAllowed;
096
097    private final boolean copyFindFreeNameDisabled;
098
099    public SQLSession(SessionImpl session, Repository repository) {
100        super(repository);
101        this.session = session;
102        negativeAclAllowed = Framework.isBooleanPropertyTrue(ALLOW_NEGATIVE_ACL_PROPERTY);
103        copyFindFreeNameDisabled = Framework.isBooleanPropertyTrue(COPY_FINDFREENAME_DISABLED_PROP);
104    }
105
106    /*
107     * ----- org.nuxeo.ecm.core.model.Session -----
108     */
109
110    @Override
111    public Document getRootDocument() {
112        return newDocument(session.getRootNode());
113    }
114
115    @Override
116    public Document getNullDocument() {
117        return new SQLDocumentLive(null, null, this, true);
118    }
119
120    @Override
121    public void destroy() {
122        session.close();
123    }
124
125    @Override
126    public void save() {
127        session.save();
128    }
129
130    @Override
131    public String getRepositoryName() {
132        return repository.getName();
133    }
134
135    protected String idToString(Serializable id) {
136        return session.getModel().idToString(id);
137    }
138
139    protected Serializable idFromString(String id) {
140        return session.getModel().idFromString(id);
141    }
142
143    @Override
144    public ScrollResult<String> scroll(String query, int batchSize, int keepAliveSeconds) {
145        return session.scroll(query, batchSize, keepAliveSeconds);
146    }
147
148    @Override
149    public ScrollResult<String> scroll(String query, QueryFilter queryFilter, int batchSize, int keepAliveSeconds) {
150        return session.scroll(query, queryFilter, batchSize, keepAliveSeconds);
151    }
152
153    @Override
154    public ScrollResult<String> scroll(String scrollId) {
155        return session.scroll(scrollId);
156    }
157
158    @Override
159    public Document getDocumentByUUID(String uuid) throws DocumentNotFoundException {
160        /*
161         * Document ids coming from higher level have been turned into strings (by {@link SQLDocument#getUUID}) but the
162         * backend may actually expect them to be Longs (for database-generated integer ids).
163         */
164        Document doc = getDocumentById(idFromString(uuid));
165        if (doc == null) {
166            // required by callers such as AbstractSession.exists
167            throw new DocumentNotFoundException(uuid);
168        }
169        return doc;
170    }
171
172    @Override
173    public Document resolvePath(String path) throws DocumentNotFoundException {
174        if (path.endsWith("/") && path.length() > 1) {
175            path = path.substring(0, path.length() - 1);
176        }
177        Node node = session.getNodeByPath(path, session.getRootNode());
178        Document doc = newDocument(node);
179        if (doc == null) {
180            throw new DocumentNotFoundException(path);
181        }
182        return doc;
183    }
184
185    protected void orderBefore(Node node, Node src, Node dest) {
186        session.orderBefore(node, src, dest);
187    }
188
189    @Override
190    public Document move(Document source, Document parent, String name) {
191        assert source instanceof SQLDocument;
192        assert parent instanceof SQLDocument;
193        if (name == null) {
194            name = source.getName();
195        }
196        Node result = session.move(((SQLDocument) source).getNode(), ((SQLDocument) parent).getNode(), name);
197        return newDocument(result);
198    }
199
200    private static final Pattern dotDigitsPattern = Pattern.compile("(.*)\\.[0-9]+$");
201
202    protected String findFreeName(Node parentNode, String name) {
203        if (session.hasChildNode(parentNode, name, false)) {
204            Matcher m = dotDigitsPattern.matcher(name);
205            if (m.matches()) {
206                // remove trailing dot and digits
207                name = m.group(1);
208            }
209            // add dot + unique digits
210            name += "." + System.nanoTime();
211        }
212        return name;
213    }
214
215    @Override
216    public Document copy(Document source, Document parent, String name) {
217        assert source instanceof SQLDocument;
218        assert parent instanceof SQLDocument;
219        if (name == null) {
220            name = source.getName();
221        }
222        Node parentNode = ((SQLDocument) parent).getNode();
223        if (!copyFindFreeNameDisabled) {
224            name = findFreeName(parentNode, name);
225        }
226        Node copy = session.copy(((SQLDocument) source).getNode(), parentNode, name,
227                this::notifyDocumentBlobManagerAfterCopy);
228        return newDocument(copy);
229    }
230
231    protected void notifyDocumentBlobManagerAfterCopy(Node node) {
232        Document doc = newDocument(node);
233        notifyAfterCopy(doc);
234    }
235
236    @Override
237    public Document getVersion(String versionableId, VersionModel versionModel) {
238        Serializable vid = idFromString(versionableId);
239        Node versionNode = session.getVersionByLabel(vid, versionModel.getLabel());
240        if (versionNode == null) {
241            return null;
242        }
243        versionModel.setDescription(versionNode.getSimpleProperty(Model.VERSION_DESCRIPTION_PROP).getString());
244        versionModel.setCreated((Calendar) versionNode.getSimpleProperty(Model.VERSION_CREATED_PROP).getValue());
245        return newDocument(versionNode);
246    }
247
248    @Override
249    public Document createProxy(Document doc, Document folder) {
250        Node folderNode = ((SQLDocument) folder).getNode();
251        Node targetNode = ((SQLDocument) doc).getNode();
252        Serializable targetId = targetNode.getId();
253        Serializable versionableId;
254        if (doc.isVersion()) {
255            versionableId = targetNode.getSimpleProperty(Model.VERSION_VERSIONABLE_PROP).getValue();
256        } else if (doc.isProxy()) {
257            // copy the proxy
258            targetId = targetNode.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue();
259            versionableId = targetNode.getSimpleProperty(Model.PROXY_VERSIONABLE_PROP).getValue();
260        } else {
261            // working copy (live document)
262            versionableId = targetId;
263        }
264        String name = findFreeName(folderNode, doc.getName());
265        Node proxy = session.addProxy(targetId, versionableId, folderNode, name, null);
266        return newDocument(proxy);
267    }
268
269    @Override
270    public List<Document> getProxies(Document document, Document parent) {
271        List<Node> proxyNodes = session.getProxies(((SQLDocument) document).getNode(),
272                parent == null ? null : ((SQLDocument) parent).getNode());
273        List<Document> proxies = new ArrayList<>(proxyNodes.size());
274        for (Node proxyNode : proxyNodes) {
275            proxies.add(newDocument(proxyNode));
276        }
277        return proxies;
278    }
279
280    @Override
281    public List<Document> getProxies(Document doc) {
282        List<Node> proxyNodes = session.getProxies(((SQLDocument) doc).getNode());
283        return proxyNodes.stream().map(this::newDocument).collect(Collectors.toList());
284    }
285
286    @Override
287    public void setProxyTarget(Document proxy, Document target) {
288        Node proxyNode = ((SQLDocument) proxy).getNode();
289        Serializable targetId = idFromString(target.getUUID());
290        session.setProxyTarget(proxyNode, targetId);
291    }
292
293    // returned document is r/w even if a version or a proxy, so that normal
294    // props can be set
295    @Override
296    public Document importDocument(String uuid, Document parent, String name, String typeName,
297            Map<String, Serializable> properties) {
298        boolean isProxy = typeName.equals(Model.PROXY_TYPE);
299        Map<String, Serializable> props = new HashMap<>();
300        Long pos = null; // TODO pos
301        if (!isProxy) {
302            // version & live document
303            props.put(Model.MISC_LIFECYCLE_POLICY_PROP, properties.get(CoreSession.IMPORT_LIFECYCLE_POLICY));
304            props.put(Model.MISC_LIFECYCLE_STATE_PROP, properties.get(CoreSession.IMPORT_LIFECYCLE_STATE));
305
306            Serializable importLockOwnerProp = properties.get(CoreSession.IMPORT_LOCK_OWNER);
307            if (importLockOwnerProp != null) {
308                props.put(Model.LOCK_OWNER_PROP, importLockOwnerProp);
309            }
310            Serializable importLockCreatedProp = properties.get(CoreSession.IMPORT_LOCK_CREATED);
311            if (importLockCreatedProp != null) {
312                props.put(Model.LOCK_CREATED_PROP, importLockCreatedProp);
313            }
314            props.put(Model.MAIN_IS_RETENTION_ACTIVE_PROP, properties.get(CoreSession.IMPORT_IS_RETENTION_ACTIVE));
315
316            props.put(Model.MAIN_MAJOR_VERSION_PROP, properties.get(CoreSession.IMPORT_VERSION_MAJOR));
317            props.put(Model.MAIN_MINOR_VERSION_PROP, properties.get(CoreSession.IMPORT_VERSION_MINOR));
318            props.put(Model.MAIN_IS_VERSION_PROP, properties.get(CoreSession.IMPORT_IS_VERSION));
319        }
320        Node parentNode;
321        if (parent == null) {
322            // version
323            parentNode = null;
324            props.put(Model.VERSION_VERSIONABLE_PROP,
325                    idFromString((String) properties.get(CoreSession.IMPORT_VERSION_VERSIONABLE_ID)));
326            props.put(Model.VERSION_CREATED_PROP, properties.get(CoreSession.IMPORT_VERSION_CREATED));
327            props.put(Model.VERSION_LABEL_PROP, properties.get(CoreSession.IMPORT_VERSION_LABEL));
328            props.put(Model.VERSION_DESCRIPTION_PROP, properties.get(CoreSession.IMPORT_VERSION_DESCRIPTION));
329            props.put(Model.VERSION_IS_LATEST_PROP, properties.get(CoreSession.IMPORT_VERSION_IS_LATEST));
330            props.put(Model.VERSION_IS_LATEST_MAJOR_PROP, properties.get(CoreSession.IMPORT_VERSION_IS_LATEST_MAJOR));
331        } else {
332            parentNode = ((SQLDocument) parent).getNode();
333            if (isProxy) {
334                // proxy
335                props.put(Model.PROXY_TARGET_PROP,
336                        idFromString((String) properties.get(CoreSession.IMPORT_PROXY_TARGET_ID)));
337                props.put(Model.PROXY_VERSIONABLE_PROP,
338                        idFromString((String) properties.get(CoreSession.IMPORT_PROXY_VERSIONABLE_ID)));
339            } else {
340                // live document
341                props.put(Model.MAIN_BASE_VERSION_PROP,
342                        idFromString((String) properties.get(CoreSession.IMPORT_BASE_VERSION_ID)));
343                props.put(Model.MAIN_CHECKED_IN_PROP, properties.get(CoreSession.IMPORT_CHECKED_IN));
344            }
345        }
346        return importChild(uuid, parentNode, name, pos, typeName, props);
347    }
348
349    protected static final Pattern ORDER_BY_PATH_ASC = Pattern.compile(
350            "(.*)\\s+ORDER\\s+BY\\s+" + NXQL.ECM_PATH + "\\s*$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
351
352    protected static final Pattern ORDER_BY_PATH_DESC = Pattern.compile(
353            "(.*)\\s+ORDER\\s+BY\\s+" + NXQL.ECM_PATH + "\\s+DESC\\s*$", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
354
355    @Override
356    public PartialList<Document> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) {
357        // do ORDER BY ecm:path by hand in SQLQueryResult as we can't
358        // do it in SQL (and has to do limit/offset as well)
359        Boolean orderByPath;
360        Matcher matcher = ORDER_BY_PATH_ASC.matcher(query);
361        if (matcher.matches()) {
362            orderByPath = Boolean.TRUE; // ASC
363        } else {
364            matcher = ORDER_BY_PATH_DESC.matcher(query);
365            if (matcher.matches()) {
366                orderByPath = Boolean.FALSE; // DESC
367            } else {
368                orderByPath = null;
369            }
370        }
371        long limit = 0;
372        long offset = 0;
373        if (orderByPath != null) {
374            query = matcher.group(1);
375            limit = queryFilter.getLimit();
376            offset = queryFilter.getOffset();
377            queryFilter = QueryFilter.withoutLimitOffset(queryFilter);
378        }
379        PartialList<Serializable> pl = session.query(query, queryType, queryFilter, countUpTo);
380
381        // get Documents in bulk, returns a newly-allocated ArrayList
382        List<Document> list = getDocumentsById(pl);
383
384        // order / limit
385        if (orderByPath != null) {
386            Collections.sort(list, new PathComparator(orderByPath.booleanValue()));
387        }
388        if (limit != 0) {
389            // do limit/offset by hand
390            int size = list.size();
391            list.subList(0, (int) (offset > size ? size : offset)).clear();
392            size = list.size();
393            if (limit < size) {
394                list.subList((int) limit, size).clear();
395            }
396        }
397        return new PartialList<>(list, pl.totalSize());
398    }
399
400    public static class PathComparator implements Comparator<Document> {
401
402        private final int sign;
403
404        public PathComparator(boolean asc) {
405            this.sign = asc ? 1 : -1;
406        }
407
408        @Override
409        public int compare(Document doc1, Document doc2) {
410            String p1 = doc1.getPath();
411            String p2 = doc2.getPath();
412            if (p1 == null && p2 == null) {
413                return sign * doc1.getUUID().compareTo(doc2.getUUID());
414            } else if (p1 == null) {
415                return sign;
416            } else if (p2 == null) {
417                return -1 * sign;
418            }
419            return sign * p1.compareTo(p2);
420        }
421    }
422
423    @Override
424    public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter,
425            boolean distinctDocuments, Object[] params) {
426        return session.queryAndFetch(query, queryType, queryFilter, distinctDocuments, params);
427    }
428
429    @Override
430    public PartialList<Map<String, Serializable>> queryProjection(String query, String queryType,
431            QueryFilter queryFilter, boolean distinctDocuments, long countUpTo, Object[] params) {
432        return session.queryProjection(query, queryType, queryFilter, distinctDocuments, countUpTo, params);
433    }
434
435    /*
436     * ----- called by SQLDocument -----
437     */
438
439    private Document newDocument(Node node) {
440        return newDocument(node, true);
441    }
442
443    // "readonly" meaningful for proxies and versions, used for import
444    private Document newDocument(Node node, boolean readonly) {
445        if (node == null) {
446            // root's parent
447            return null;
448        }
449
450        Node targetNode = null;
451        String typeName = node.getPrimaryType();
452        if (node.isProxy()) {
453            Serializable targetId = node.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue();
454            if (targetId == null) {
455                throw new DocumentNotFoundException("Proxy has null target");
456            }
457            targetNode = session.getNodeById(targetId);
458            typeName = targetNode.getPrimaryType();
459        }
460        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
461        DocumentType type = schemaManager.getDocumentType(typeName);
462        if (type == null) {
463            throw new DocumentNotFoundException("Unknown document type: " + typeName);
464        }
465
466        if (node.isProxy()) {
467            // proxy seen as a normal document
468            Document proxy = new SQLDocumentLive(node, type, this, false);
469            Document target = newDocument(targetNode, readonly);
470            return new SQLDocumentProxy(proxy, target);
471        } else if (node.isVersion()) {
472            return new SQLDocumentVersion(node, type, this, readonly);
473        } else {
474            return new SQLDocumentLive(node, type, this, false);
475        }
476    }
477
478    // called by SQLQueryResult iterator & others
479    protected Document getDocumentById(Serializable id) {
480        Node node = session.getNodeById(id);
481        return node == null ? null : newDocument(node);
482    }
483
484    // called by SQLQueryResult iterator
485    protected List<Document> getDocumentsById(List<Serializable> ids) {
486        List<Document> docs = new ArrayList<>(ids.size());
487        List<Node> nodes = session.getNodesByIds(ids);
488        for (int index = 0; index < ids.size(); ++index) {
489            Node eachNode = nodes.get(index);
490            if (eachNode == null) {
491                if (log.isTraceEnabled()) {
492                    Serializable id = ids.get(index);
493                    log.trace("Cannot fetch document with id: " + id, new Throwable("debug stack trace"));
494                }
495                continue;
496            }
497            Document doc;
498            try {
499                doc = newDocument(eachNode);
500            } catch (DocumentNotFoundException e) {
501                // unknown type in db or null proxy target, ignore
502                continue;
503            }
504            docs.add(doc);
505        }
506        return docs;
507    }
508
509    protected Document getParent(Node node) {
510        return newDocument(session.getParentNode(node));
511    }
512
513    protected String getPath(Node node) {
514        return session.getPath(node);
515    }
516
517    protected Document getChild(Node node, String name) throws DocumentNotFoundException {
518        Node childNode = session.getChildNode(node, name, false);
519        Document doc = newDocument(childNode);
520        if (doc == null) {
521            throw new DocumentNotFoundException(name);
522        }
523        return doc;
524    }
525
526    protected Node getChildProperty(Node node, String name, String typeName) {
527        // all complex property children have already been created by SessionImpl.addChildNode or
528        // SessionImpl.addMixinType
529        // if one is missing here, it means that it was concurrently deleted and we're only now finding out
530        // or that a schema change was done and we now expect a new child
531        // return null in that case
532        return session.getChildNode(node, name, true);
533    }
534
535    protected Node getChildPropertyForWrite(Node node, String name, String typeName) {
536        Node childNode = getChildProperty(node, name, typeName);
537        if (childNode == null) {
538            // create the needed complex property immediately
539            childNode = session.addChildNode(node, name, null, typeName, true);
540        }
541        return childNode;
542    }
543
544    protected List<Document> getChildren(Node node) {
545        List<Node> nodes = session.getChildren(node, null, false);
546        List<Document> children = new ArrayList<>(nodes.size());
547        for (Node n : nodes) {
548            try {
549                children.add(newDocument(n));
550            } catch (DocumentNotFoundException e) {
551                // ignore error retrieving one of the children
552                continue;
553            }
554        }
555        return children;
556    }
557
558    protected boolean hasChild(Node node, String name) {
559        return session.hasChildNode(node, name, false);
560    }
561
562    protected boolean hasChildren(Node node) {
563        return session.hasChildren(node, false);
564    }
565
566    protected Document addChild(Node parent, String name, Long pos, String typeName) {
567        return newDocument(session.addChildNode(parent, name, pos, typeName, false));
568    }
569
570    protected Node addChildProperty(Node parent, String name, Long pos, String typeName) {
571        return session.addChildNode(parent, name, pos, typeName, true);
572    }
573
574    protected Document importChild(String uuid, Node parent, String name, Long pos, String typeName,
575            Map<String, Serializable> props) {
576        Serializable id = idFromString(uuid);
577        Node node = session.addChildNode(id, parent, name, pos, typeName, false);
578        for (Entry<String, Serializable> entry : props.entrySet()) {
579            node.setSimpleProperty(entry.getKey(), entry.getValue());
580        }
581        return newDocument(node, false); // not readonly
582    }
583
584    protected boolean addMixinType(Node node, String mixin) {
585        return session.addMixinType(node, mixin);
586    }
587
588    protected boolean removeMixinType(Node node, String mixin) {
589        return session.removeMixinType(node, mixin);
590    }
591
592    protected List<Node> getComplexList(Node node, String name) {
593        List<Node> nodes = session.getChildren(node, name, true);
594        return nodes;
595    }
596
597    protected void remove(Node node) {
598        session.removeNode(node, this::notifyDocumentBlobManagerBeforeRemove);
599    }
600
601    protected void notifyDocumentBlobManagerBeforeRemove(Node node) {
602        Document doc = newDocument(node);
603        getDocumentBlobManager().notifyBeforeRemove(doc);
604    }
605
606    protected void removeProperty(Node node) {
607        session.removePropertyNode(node);
608    }
609
610    protected Document checkIn(Node node, String label, String checkinComment) {
611        Node versionNode = session.checkIn(node, label, checkinComment);
612        return versionNode == null ? null : newDocument(versionNode);
613    }
614
615    protected void checkOut(Node node) {
616        session.checkOut(node);
617    }
618
619    protected void restore(Node node, Node version) {
620        if (node.isRecord()) {
621            notifyDocumentBlobManagerBeforeRemove(node);
622        }
623        session.restore(node, version);
624        if (version.isRecord()) {
625            notifyDocumentBlobManagerAfterCopy(node);
626        }
627    }
628
629    protected Document getVersionByLabel(String versionSeriesId, String label) {
630        Serializable vid = idFromString(versionSeriesId);
631        Node versionNode = session.getVersionByLabel(vid, label);
632        return versionNode == null ? null : newDocument(versionNode);
633    }
634
635    protected List<Document> getVersions(String versionSeriesId) {
636        Serializable vid = idFromString(versionSeriesId);
637        List<Node> versionNodes = session.getVersions(vid);
638        List<Document> versions = new ArrayList<>(versionNodes.size());
639        for (Node versionNode : versionNodes) {
640            versions.add(newDocument(versionNode));
641        }
642        return versions;
643    }
644
645    public Document getLastVersion(String versionSeriesId) {
646        Serializable vid = idFromString(versionSeriesId);
647        Node versionNode = session.getLastVersion(vid);
648        if (versionNode == null) {
649            return null;
650        }
651        return newDocument(versionNode);
652    }
653
654    protected Node getNodeById(Serializable id) {
655        return session.getNodeById(id);
656    }
657
658    @Override
659    public LockManager getLockManager() {
660        return session.getLockManager();
661    }
662
663    @Override
664    public boolean isNegativeAclAllowed() {
665        return negativeAclAllowed;
666    }
667
668    @Override
669    public void updateReadACLs(Collection<String> docIds) {
670        throw new UnsupportedOperationException();
671    }
672
673    @Override
674    public void setACP(Document doc, ACP acp, boolean overwrite) {
675        if (!overwrite && acp == null) {
676            return;
677        }
678        checkNegativeAcl(acp);
679        Node node = ((SQLDocument) doc).getNode();
680        ACLRow[] aclrows;
681        if (overwrite) {
682            aclrows = acp == null ? null : acpToAclRows(acp);
683        } else {
684            aclrows = (ACLRow[]) node.getCollectionProperty(Model.ACL_PROP).getValue();
685            aclrows = updateAclRows(aclrows, acp);
686        }
687        node.getCollectionProperty(Model.ACL_PROP).setValue(aclrows);
688        session.requireReadAclsUpdate();
689    }
690
691    /*
692     * ----- internal methods -----
693     */
694
695    @Override
696    public ACP getACP(Document doc) {
697        Node node = ((SQLDocument) doc).getNode();
698        ACLRow[] aclrows = (ACLRow[]) node.getCollectionProperty(Model.ACL_PROP).getValue();
699        return aclRowsToACP(aclrows);
700    }
701
702    // unit tested
703    protected static ACP aclRowsToACP(ACLRow[] acls) {
704        ACP acp = new ACPImpl();
705        ACL acl = null;
706        String name = null;
707        for (ACLRow aclrow : acls) {
708            if (!aclrow.name.equals(name)) {
709                if (acl != null) {
710                    acp.addACL(acl);
711                }
712                name = aclrow.name;
713                acl = new ACLImpl(name);
714            }
715            // XXX should prefix user/group
716            String user = aclrow.user;
717            if (user == null) {
718                user = aclrow.group;
719            }
720            acl.add(ACE.builder(user, aclrow.permission)
721                       .isGranted(aclrow.grant)
722                       .creator(aclrow.creator)
723                       .begin(aclrow.begin)
724                       .end(aclrow.end)
725                       .build());
726        }
727        if (acl != null) {
728            acp.addACL(acl);
729        }
730        return acp;
731    }
732
733    // unit tested
734    protected static ACLRow[] acpToAclRows(ACP acp) {
735        List<ACLRow> aclrows = new LinkedList<>();
736        for (ACL acl : acp.getACLs()) {
737            String name = acl.getName();
738            if (name.equals(ACL.INHERITED_ACL)) {
739                continue;
740            }
741            for (ACE ace : acl.getACEs()) {
742                addACLRow(aclrows, name, ace);
743            }
744        }
745        ACLRow[] array = new ACLRow[aclrows.size()];
746        return aclrows.toArray(array);
747    }
748
749    // unit tested
750    protected static ACLRow[] updateAclRows(ACLRow[] aclrows, ACP acp) {
751        List<ACLRow> newaclrows = new LinkedList<>();
752        Map<String, ACL> aclmap = new HashMap<>();
753        for (ACL acl : acp.getACLs()) {
754            String name = acl.getName();
755            if (ACL.INHERITED_ACL.equals(name)) {
756                continue;
757            }
758            aclmap.put(name, acl);
759        }
760        List<ACE> aces = Collections.emptyList();
761        Set<String> aceKeys = null;
762        String name = null;
763        for (ACLRow aclrow : aclrows) {
764            // new acl?
765            if (!aclrow.name.equals(name)) {
766                // finish remaining aces
767                for (ACE ace : aces) {
768                    addACLRow(newaclrows, name, ace);
769                }
770                // start next round
771                name = aclrow.name;
772                ACL acl = aclmap.remove(name);
773                aces = acl == null ? Collections.<ACE> emptyList() : new LinkedList<>(Arrays.asList(acl.getACEs()));
774                aceKeys = new HashSet<>();
775                for (ACE ace : aces) {
776                    aceKeys.add(getACEkey(ace));
777                }
778            }
779            if (!aceKeys.contains(getACLrowKey(aclrow))) {
780                // no match, keep the aclrow info instead of the ace
781                newaclrows.add(new ACLRow(newaclrows.size(), name, aclrow.grant, aclrow.permission, aclrow.user,
782                        aclrow.group, aclrow.creator, aclrow.begin, aclrow.end, aclrow.status));
783            }
784        }
785        // finish remaining aces for last acl done
786        for (ACE ace : aces) {
787            addACLRow(newaclrows, name, ace);
788        }
789        // do non-done acls
790        for (ACL acl : aclmap.values()) {
791            name = acl.getName();
792            for (ACE ace : acl.getACEs()) {
793                addACLRow(newaclrows, name, ace);
794            }
795        }
796        ACLRow[] array = new ACLRow[newaclrows.size()];
797        return newaclrows.toArray(array);
798    }
799
800    /** Key to distinguish ACEs */
801    protected static String getACEkey(ACE ace) {
802        // TODO separate user/group
803        return ace.getUsername() + '|' + ace.getPermission();
804    }
805
806    /** Key to distinguish ACLRows */
807    protected static String getACLrowKey(ACLRow aclrow) {
808        // TODO separate user/group
809        String user = aclrow.user;
810        if (user == null) {
811            user = aclrow.group;
812        }
813        return user + '|' + aclrow.permission;
814    }
815
816    protected static void addACLRow(List<ACLRow> aclrows, String name, ACE ace) {
817        // XXX should prefix user/group
818        String user = ace.getUsername();
819        if (user == null) {
820            // JCR implementation logs null and skips it
821            return;
822        }
823        String group = null; // XXX all in user for now
824        aclrows.add(new ACLRow(aclrows.size(), name, ace.isGranted(), ace.getPermission(), user, group,
825                ace.getCreator(), ace.getBegin(), ace.getEnd(), ace.getLongStatus()));
826    }
827
828    @Override
829    public boolean isFulltextStoredInBlob() {
830        return session.isFulltextStoredInBlob();
831    }
832
833    @Override
834    public Map<String, String> getBinaryFulltext(String id) {
835        Document doc;
836        FulltextConfiguration fulltextConfiguration = repository.getFulltextConfiguration();
837        if (fulltextConfiguration != null && fulltextConfiguration.isFulltextStoredInBlob()) {
838            // only in this case, the doc is needed to retrieve the blob
839            doc = getDocumentById(id);
840        } else {
841            doc = null;
842        }
843        return session.getBinaryFulltext(idFromString(id), doc);
844    }
845
846    @Override
847    public void removeDocument(String id) {
848        throw new UnsupportedOperationException("Not implemented yet");
849    }
850
851    public boolean isChangeTokenEnabled() {
852        return session.isChangeTokenEnabled();
853    }
854
855    public void markUserChange(Serializable id) {
856        session.markUserChange(id);
857    }
858
859    /*
860     * ----- Transaction management -----
861     */
862
863    @Override
864    public void start() {
865        session.start();
866    }
867
868    @Override
869    public void end() {
870        session.end();
871    }
872
873    @Override
874    public void commit() {
875        session.commit();
876    }
877
878    @Override
879    public void rollback() {
880        session.rollback();
881    }
882
883}