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