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