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