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