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