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, Object[] params) { 442 return session.queryAndFetch(query, queryType, queryFilter, params); 443 } 444 445 /* 446 * ----- called by SQLDocument ----- 447 */ 448 449 private Document newDocument(Node node) { 450 return newDocument(node, true); 451 } 452 453 // "readonly" meaningful for proxies and versions, used for import 454 private Document newDocument(Node node, boolean readonly) { 455 if (node == null) { 456 // root's parent 457 return null; 458 } 459 460 Node targetNode = null; 461 String typeName = node.getPrimaryType(); 462 if (node.isProxy()) { 463 Serializable targetId = node.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue(); 464 if (targetId == null) { 465 throw new DocumentNotFoundException("Proxy has null target"); 466 } 467 targetNode = session.getNodeById(targetId); 468 typeName = targetNode.getPrimaryType(); 469 } 470 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 471 DocumentType type = schemaManager.getDocumentType(typeName); 472 if (type == null) { 473 throw new DocumentNotFoundException("Unknown document type: " + typeName); 474 } 475 476 if (node.isProxy()) { 477 // proxy seen as a normal document 478 Document proxy = new SQLDocumentLive(node, type, this, false); 479 Document target = newDocument(targetNode, readonly); 480 return new SQLDocumentProxy(proxy, target); 481 } else if (node.isVersion()) { 482 return new SQLDocumentVersion(node, type, this, readonly); 483 } else { 484 return new SQLDocumentLive(node, type, this, false); 485 } 486 } 487 488 // called by SQLQueryResult iterator & others 489 protected Document getDocumentById(Serializable id) { 490 Node node = session.getNodeById(id); 491 return node == null ? null : newDocument(node); 492 } 493 494 // called by SQLQueryResult iterator 495 protected List<Document> getDocumentsById(List<Serializable> ids) { 496 List<Document> docs = new ArrayList<Document>(ids.size()); 497 List<Node> nodes = session.getNodesByIds(ids); 498 for (int index = 0; index < ids.size(); ++index) { 499 Node eachNode = nodes.get(index); 500 if (eachNode == null) { 501 Serializable eachId = ids.get(index); 502 log.warn("Cannot fetch document by id " + eachId, new Throwable("debug stack trace")); 503 continue; 504 } 505 Document doc; 506 try { 507 doc = newDocument(eachNode); 508 } catch (DocumentNotFoundException e) { 509 // unknown type in db, ignore 510 continue; 511 } 512 docs.add(doc); 513 } 514 return docs; 515 } 516 517 protected Document getParent(Node node) { 518 return newDocument(session.getParentNode(node)); 519 } 520 521 protected String getPath(Node node) { 522 return session.getPath(node); 523 } 524 525 protected Document getChild(Node node, String name) throws DocumentNotFoundException { 526 Node childNode = session.getChildNode(node, name, false); 527 Document doc = newDocument(childNode); 528 if (doc == null) { 529 throw new DocumentNotFoundException(name); 530 } 531 return doc; 532 } 533 534 protected Node getChildProperty(Node node, String name, String typeName) { 535 // all complex property children have already been created by SessionImpl.addChildNode or 536 // SessionImpl.addMixinType 537 // if one is missing here, it means that it was concurrently deleted and we're only now finding out 538 // or that a schema change was done and we now expect a new child 539 // return null in that case 540 return session.getChildNode(node, name, true); 541 } 542 543 protected Node getChildPropertyForWrite(Node node, String name, String typeName) { 544 Node childNode = getChildProperty(node, name, typeName); 545 if (childNode == null) { 546 // create the needed complex property immediately 547 childNode = session.addChildNode(node, name, null, typeName, true); 548 } 549 return childNode; 550 } 551 552 protected List<Document> getChildren(Node node) { 553 List<Node> nodes = session.getChildren(node, null, false); 554 List<Document> children = new ArrayList<Document>(nodes.size()); 555 for (Node n : nodes) { 556 try { 557 children.add(newDocument(n)); 558 } catch (DocumentNotFoundException e) { 559 // ignore error retrieving one of the children 560 continue; 561 } 562 } 563 return children; 564 } 565 566 protected boolean hasChild(Node node, String name) { 567 return session.hasChildNode(node, name, false); 568 } 569 570 protected boolean hasChildren(Node node) { 571 return session.hasChildren(node, false); 572 } 573 574 protected Document addChild(Node parent, String name, Long pos, String typeName) { 575 return newDocument(session.addChildNode(parent, name, pos, typeName, false)); 576 } 577 578 protected Node addChildProperty(Node parent, String name, Long pos, String typeName) { 579 return session.addChildNode(parent, name, pos, typeName, true); 580 } 581 582 protected Document importChild(String uuid, Node parent, String name, Long pos, String typeName, 583 Map<String, Serializable> props) { 584 Serializable id = idFromString(uuid); 585 Node node = session.addChildNode(id, parent, name, pos, typeName, false); 586 for (Entry<String, Serializable> entry : props.entrySet()) { 587 node.setSimpleProperty(entry.getKey(), entry.getValue()); 588 } 589 return newDocument(node, false); // not readonly 590 } 591 592 protected boolean addMixinType(Node node, String mixin) { 593 return session.addMixinType(node, mixin); 594 } 595 596 protected boolean removeMixinType(Node node, String mixin) { 597 return session.removeMixinType(node, mixin); 598 } 599 600 protected List<Node> getComplexList(Node node, String name) { 601 List<Node> nodes = session.getChildren(node, name, true); 602 return nodes; 603 } 604 605 protected void remove(Node node) { 606 session.removeNode(node); 607 } 608 609 protected void removeProperty(Node node) { 610 session.removePropertyNode(node); 611 } 612 613 protected Document checkIn(Node node, String label, String checkinComment) { 614 Node versionNode = session.checkIn(node, label, checkinComment); 615 return versionNode == null ? null : newDocument(versionNode); 616 } 617 618 protected void checkOut(Node node) { 619 session.checkOut(node); 620 } 621 622 protected void restore(Node node, Node version) { 623 session.restore(node, version); 624 } 625 626 protected Document getVersionByLabel(String versionSeriesId, String label) { 627 Serializable vid = idFromString(versionSeriesId); 628 Node versionNode = session.getVersionByLabel(vid, label); 629 return versionNode == null ? null : newDocument(versionNode); 630 } 631 632 protected List<Document> getVersions(String versionSeriesId) { 633 Serializable vid = idFromString(versionSeriesId); 634 List<Node> versionNodes = session.getVersions(vid); 635 List<Document> versions = new ArrayList<Document>(versionNodes.size()); 636 for (Node versionNode : versionNodes) { 637 versions.add(newDocument(versionNode)); 638 } 639 return versions; 640 } 641 642 public Document getLastVersion(String versionSeriesId) { 643 Serializable vid = idFromString(versionSeriesId); 644 Node versionNode = session.getLastVersion(vid); 645 if (versionNode == null) { 646 return null; 647 } 648 return newDocument(versionNode); 649 } 650 651 protected Node getNodeById(Serializable id) { 652 return session.getNodeById(id); 653 } 654 655 @Override 656 public LockManager getLockManager() { 657 return session.getLockManager(); 658 } 659 660 @Override 661 public boolean isNegativeAclAllowed() { 662 return negativeAclAllowed; 663 } 664 665 @Override 666 public void setACP(Document doc, ACP acp, boolean overwrite) { 667 if (!overwrite && acp == null) { 668 return; 669 } 670 checkNegativeAcl(acp); 671 Node node = ((SQLDocument) doc).getNode(); 672 ACLRow[] aclrows; 673 if (overwrite) { 674 aclrows = acp == null ? null : acpToAclRows(acp); 675 } else { 676 aclrows = (ACLRow[]) node.getCollectionProperty(Model.ACL_PROP).getValue(); 677 aclrows = updateAclRows(aclrows, acp); 678 } 679 node.getCollectionProperty(Model.ACL_PROP).setValue(aclrows); 680 session.requireReadAclsUpdate(); 681 } 682 683 protected void checkNegativeAcl(ACP acp) { 684 if (negativeAclAllowed) { 685 return; 686 } 687 if (acp == null) { 688 return; 689 } 690 for (ACL acl : acp.getACLs()) { 691 if (acl.getName().equals(ACL.INHERITED_ACL)) { 692 continue; 693 } 694 for (ACE ace : acl.getACEs()) { 695 if (ace.isGranted()) { 696 continue; 697 } 698 String permission = ace.getPermission(); 699 if (permission.equals(SecurityConstants.EVERYTHING) 700 && ace.getUsername().equals(SecurityConstants.EVERYONE)) { 701 continue; 702 } 703 // allow Write, as we're sure it doesn't include Read/Browse 704 if (permission.equals(SecurityConstants.WRITE)) { 705 continue; 706 } 707 throw new IllegalArgumentException("Negative ACL not allowed: " + ace); 708 } 709 } 710 } 711 712 @Override 713 public ACP getMergedACP(Document doc) { 714 Document base = doc.isVersion() ? doc.getSourceDocument() : doc; 715 if (base == null) { 716 return null; 717 } 718 ACP acp = getACP(base); 719 if (doc.getParent() == null) { 720 return acp; 721 } 722 // get inherited acls only if no blocking inheritance ACE exists in the top level acp. 723 ACL acl = null; 724 if (acp == null || acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) != Access.DENY) { 725 acl = getInheritedACLs(doc); 726 } 727 if (acp == null) { 728 if (acl == null) { 729 return null; 730 } 731 acp = new ACPImpl(); 732 } 733 if (acl != null) { 734 acp.addACL(acl); 735 } 736 return acp; 737 } 738 739 /* 740 * ----- internal methods ----- 741 */ 742 743 protected ACP getACP(Document doc) { 744 Node node = ((SQLDocument) doc).getNode(); 745 ACLRow[] aclrows = (ACLRow[]) node.getCollectionProperty(Model.ACL_PROP).getValue(); 746 return aclRowsToACP(aclrows); 747 } 748 749 // unit tested 750 protected static ACP aclRowsToACP(ACLRow[] acls) { 751 ACP acp = new ACPImpl(); 752 ACL acl = null; 753 String name = null; 754 for (ACLRow aclrow : acls) { 755 if (!aclrow.name.equals(name)) { 756 if (acl != null) { 757 acp.addACL(acl); 758 } 759 name = aclrow.name; 760 acl = new ACLImpl(name); 761 } 762 // XXX should prefix user/group 763 String user = aclrow.user; 764 if (user == null) { 765 user = aclrow.group; 766 } 767 acl.add(ACE.builder(user, aclrow.permission) 768 .isGranted(aclrow.grant) 769 .creator(aclrow.creator) 770 .begin(aclrow.begin) 771 .end(aclrow.end) 772 .build()); 773 } 774 if (acl != null) { 775 acp.addACL(acl); 776 } 777 return acp; 778 } 779 780 // unit tested 781 protected static ACLRow[] acpToAclRows(ACP acp) { 782 List<ACLRow> aclrows = new LinkedList<ACLRow>(); 783 for (ACL acl : acp.getACLs()) { 784 String name = acl.getName(); 785 if (name.equals(ACL.INHERITED_ACL)) { 786 continue; 787 } 788 for (ACE ace : acl.getACEs()) { 789 addACLRow(aclrows, name, ace); 790 } 791 } 792 ACLRow[] array = new ACLRow[aclrows.size()]; 793 return aclrows.toArray(array); 794 } 795 796 // unit tested 797 protected static ACLRow[] updateAclRows(ACLRow[] aclrows, ACP acp) { 798 List<ACLRow> newaclrows = new LinkedList<ACLRow>(); 799 Map<String, ACL> aclmap = new HashMap<String, ACL>(); 800 for (ACL acl : acp.getACLs()) { 801 String name = acl.getName(); 802 if (ACL.INHERITED_ACL.equals(name)) { 803 continue; 804 } 805 aclmap.put(name, acl); 806 } 807 List<ACE> aces = Collections.emptyList(); 808 Set<String> aceKeys = null; 809 String name = null; 810 for (ACLRow aclrow : aclrows) { 811 // new acl? 812 if (!aclrow.name.equals(name)) { 813 // finish remaining aces 814 for (ACE ace : aces) { 815 addACLRow(newaclrows, name, ace); 816 } 817 // start next round 818 name = aclrow.name; 819 ACL acl = aclmap.remove(name); 820 aces = acl == null ? Collections.<ACE> emptyList() : new LinkedList<ACE>(Arrays.asList(acl.getACEs())); 821 aceKeys = new HashSet<String>(); 822 for (ACE ace : aces) { 823 aceKeys.add(getACEkey(ace)); 824 } 825 } 826 if (!aceKeys.contains(getACLrowKey(aclrow))) { 827 // no match, keep the aclrow info instead of the ace 828 newaclrows.add(new ACLRow(newaclrows.size(), name, aclrow.grant, aclrow.permission, aclrow.user, 829 aclrow.group, aclrow.creator, aclrow.begin, aclrow.end, aclrow.status)); 830 } 831 } 832 // finish remaining aces for last acl done 833 for (ACE ace : aces) { 834 addACLRow(newaclrows, name, ace); 835 } 836 // do non-done acls 837 for (ACL acl : aclmap.values()) { 838 name = acl.getName(); 839 for (ACE ace : acl.getACEs()) { 840 addACLRow(newaclrows, name, ace); 841 } 842 } 843 ACLRow[] array = new ACLRow[newaclrows.size()]; 844 return newaclrows.toArray(array); 845 } 846 847 /** Key to distinguish ACEs */ 848 protected static String getACEkey(ACE ace) { 849 // TODO separate user/group 850 return ace.getUsername() + '|' + ace.getPermission(); 851 } 852 853 /** Key to distinguish ACLRows */ 854 protected static String getACLrowKey(ACLRow aclrow) { 855 // TODO separate user/group 856 String user = aclrow.user; 857 if (user == null) { 858 user = aclrow.group; 859 } 860 return user + '|' + aclrow.permission; 861 } 862 863 protected static void addACLRow(List<ACLRow> aclrows, String name, ACE ace) { 864 // XXX should prefix user/group 865 String user = ace.getUsername(); 866 if (user == null) { 867 // JCR implementation logs null and skips it 868 return; 869 } 870 String group = null; // XXX all in user for now 871 aclrows.add(new ACLRow(aclrows.size(), name, ace.isGranted(), ace.getPermission(), user, group, 872 ace.getCreator(), ace.getBegin(), ace.getEnd(), ace.getLongStatus())); 873 } 874 875 protected ACL getInheritedACLs(Document doc) { 876 doc = doc.getParent(); 877 ACL merged = null; 878 while (doc != null) { 879 ACP acp = getACP(doc); 880 if (acp != null) { 881 ACL acl = acp.getMergedACLs(ACL.INHERITED_ACL); 882 if (merged == null) { 883 merged = acl; 884 } else { 885 merged.addAll(acl); 886 } 887 if (acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) == Access.DENY) { 888 break; 889 } 890 } 891 doc = doc.getParent(); 892 } 893 return merged; 894 } 895 896 @Override 897 public Map<String, String> getBinaryFulltext(String id) { 898 return session.getBinaryFulltext(idFromString(id)); 899 } 900 901}