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