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