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; 020 021import java.io.Serializable; 022import java.text.Normalizer; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Calendar; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.Iterator; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Map; 034import java.util.Map.Entry; 035import java.util.stream.Collectors; 036import java.util.Set; 037 038import javax.resource.ResourceException; 039import javax.resource.cci.ConnectionMetaData; 040import javax.resource.cci.Interaction; 041import javax.resource.cci.LocalTransaction; 042import javax.resource.cci.ResultSetInfo; 043import javax.transaction.xa.XAException; 044import javax.transaction.xa.XAResource; 045import javax.transaction.xa.Xid; 046 047import org.apache.commons.lang.StringUtils; 048import org.apache.commons.logging.Log; 049import org.apache.commons.logging.LogFactory; 050import org.nuxeo.ecm.core.api.ConcurrentUpdateException; 051import org.nuxeo.ecm.core.api.DocumentExistsException; 052import org.nuxeo.ecm.core.api.IterableQueryResult; 053import org.nuxeo.ecm.core.api.NuxeoException; 054import org.nuxeo.ecm.core.api.PartialList; 055import org.nuxeo.ecm.core.api.ScrollResult; 056import org.nuxeo.ecm.core.api.repository.RepositoryManager; 057import org.nuxeo.ecm.core.api.security.ACL; 058import org.nuxeo.ecm.core.api.security.SecurityConstants; 059import org.nuxeo.ecm.core.model.LockManager; 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.FulltextConfiguration; 065import org.nuxeo.ecm.core.storage.FulltextParser; 066import org.nuxeo.ecm.core.storage.FulltextUpdaterWork; 067import org.nuxeo.ecm.core.storage.FulltextUpdaterWork.IndexAndText; 068import org.nuxeo.ecm.core.storage.sql.PersistenceContext.PathAndId; 069import org.nuxeo.ecm.core.storage.sql.RowMapper.NodeInfo; 070import org.nuxeo.ecm.core.storage.sql.RowMapper.RowBatch; 071import org.nuxeo.ecm.core.storage.sql.coremodel.SQLFulltextExtractorWork; 072import org.nuxeo.ecm.core.work.api.Work; 073import org.nuxeo.ecm.core.work.api.WorkManager; 074import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling; 075import org.nuxeo.runtime.api.Framework; 076import org.nuxeo.runtime.metrics.MetricsService; 077import org.nuxeo.runtime.transaction.TransactionHelper; 078 079import com.codahale.metrics.MetricRegistry; 080import com.codahale.metrics.SharedMetricRegistries; 081import com.codahale.metrics.Timer; 082 083/** 084 * The session is the main high level access point to data from the underlying database. 085 */ 086public class SessionImpl implements Session, XAResource { 087 088 private static final Log log = LogFactory.getLog(SessionImpl.class); 089 090 /** 091 * Set this system property to false if you don't want repositories to be looked up under the compatibility name 092 * "default" in the "repositories" table. 093 * <p> 094 * Only do this if you start from an empty database, or if you have migrated the "repositories" table by hand, or if 095 * you need to create a new repository in a database already containing a "default" repository (table sharing, not 096 * recommended). 097 */ 098 public static final String COMPAT_REPOSITORY_NAME_KEY = "org.nuxeo.vcs.repository.name.default.compat"; 099 100 private static final boolean COMPAT_REPOSITORY_NAME = Boolean.parseBoolean( 101 Framework.getProperty(COMPAT_REPOSITORY_NAME_KEY, "true")); 102 103 protected final RepositoryImpl repository; 104 105 private final Mapper mapper; 106 107 private final Model model; 108 109 protected final FulltextParser fulltextParser; 110 111 // public because used by unit tests 112 public final PersistenceContext context; 113 114 protected final boolean changeTokenEnabled; 115 116 private volatile boolean live; 117 118 private boolean inTransaction; 119 120 private Node rootNode; 121 122 private long threadId; 123 124 private String threadName; 125 126 private Throwable threadStack; 127 128 private boolean readAclsChanged; 129 130 // @since 5.7 131 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 132 133 private final Timer saveTimer; 134 135 private final Timer queryTimer; 136 137 private final Timer aclrUpdateTimer; 138 139 private static final java.lang.String LOG_MIN_DURATION_KEY = "org.nuxeo.vcs.query.log_min_duration_ms"; 140 141 private static final long LOG_MIN_DURATION_NS = Long.parseLong(Framework.getProperty(LOG_MIN_DURATION_KEY, "-1")) 142 * 1000000; 143 144 public SessionImpl(RepositoryImpl repository, Model model, Mapper mapper) { 145 this.repository = repository; 146 this.mapper = mapper; 147 this.model = model; 148 context = new PersistenceContext(model, mapper, this); 149 changeTokenEnabled = repository.isChangeTokenEnabled(); 150 live = true; 151 readAclsChanged = false; 152 153 try { 154 fulltextParser = repository.fulltextParserClass.newInstance(); 155 } catch (ReflectiveOperationException e) { 156 throw new NuxeoException(e); 157 } 158 saveTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), "saves")); 159 queryTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), "queries")); 160 aclrUpdateTimer = registry.timer( 161 MetricRegistry.name("nuxeo", "repositories", repository.getName(), "aclr-updates")); 162 163 computeRootNode(); 164 } 165 166 public void checkLive() { 167 if (!live) { 168 throw new IllegalStateException("Session is not live"); 169 } 170 checkThread(); 171 } 172 173 // called by NetServlet when forwarding remote NetMapper calls. 174 @Override 175 public Mapper getMapper() { 176 return mapper; 177 } 178 179 /** 180 * Gets the XAResource. Called by the ManagedConnectionImpl, which actually wraps it in a connection-aware 181 * implementation. 182 */ 183 public XAResource getXAResource() { 184 return this; 185 } 186 187 /** 188 * Clears all the caches. Called by RepositoryManagement. 189 */ 190 protected int clearCaches() { 191 if (inTransaction) { 192 // avoid potential multi-threaded access to active session 193 return 0; 194 } 195 checkThreadEnd(); 196 return context.clearCaches(); 197 } 198 199 protected PersistenceContext getContext() { 200 return context; 201 } 202 203 protected void rollback() { 204 context.clearCaches(); 205 } 206 207 protected void checkThread() { 208 if (threadId == 0) { 209 return; 210 } 211 long currentThreadId = Thread.currentThread().getId(); 212 if (threadId == currentThreadId) { 213 return; 214 } 215 String currentThreadName = Thread.currentThread().getName(); 216 String msg = String.format( 217 "Concurrency Error: Session was started in thread %s (%s)" + " but is being used in thread %s (%s)", 218 threadId, threadName, currentThreadId, currentThreadName); 219 throw new IllegalStateException(msg, threadStack); 220 } 221 222 protected void checkThreadStart() { 223 threadId = Thread.currentThread().getId(); 224 threadName = Thread.currentThread().getName(); 225 if (log.isDebugEnabled()) { 226 threadStack = new Throwable("owner stack trace"); 227 } 228 } 229 230 protected void checkThreadEnd() { 231 threadId = 0; 232 threadName = null; 233 threadStack = null; 234 } 235 236 /** 237 * Generates a new id, or used a pre-generated one (import). 238 */ 239 protected Serializable generateNewId(Serializable id) { 240 return context.generateNewId(id); 241 } 242 243 protected boolean isIdNew(Serializable id) { 244 return context.isIdNew(id); 245 } 246 247 /* 248 * ----- javax.resource.cci.Connection ----- 249 */ 250 251 @Override 252 public void close() throws ResourceException { 253 try { 254 checkLive(); 255 closeSession(); 256 repository.closeSession(this); 257 } catch (Exception cause) { 258 throw new ResourceException(cause); 259 } 260 } 261 262 protected void closeSession() { 263 live = false; 264 context.clearCaches(); 265 // close the mapper and therefore the connection 266 mapper.close(); 267 // don't clean the caches, we keep the pristine cache around 268 // TODO this is getting destroyed, we can clean everything 269 } 270 271 @Override 272 public Interaction createInteraction() throws ResourceException { 273 throw new UnsupportedOperationException(); 274 } 275 276 @Override 277 public LocalTransaction getLocalTransaction() throws ResourceException { 278 throw new UnsupportedOperationException(); 279 } 280 281 @Override 282 public ConnectionMetaData getMetaData() throws ResourceException { 283 throw new UnsupportedOperationException(); 284 } 285 286 @Override 287 public ResultSetInfo getResultSetInfo() throws ResourceException { 288 throw new UnsupportedOperationException(); 289 } 290 291 /* 292 * ----- Session ----- 293 */ 294 295 @Override 296 public boolean isLive() { 297 return live; 298 } 299 300 @Override 301 public String getRepositoryName() { 302 return repository.getName(); 303 } 304 305 @Override 306 public Model getModel() { 307 return model; 308 } 309 310 @Override 311 public Node getRootNode() { 312 checkLive(); 313 return rootNode; 314 } 315 316 @Override 317 public void save() { 318 final Timer.Context timerContext = saveTimer.time(); 319 try { 320 checkLive(); 321 flush(); 322 if (!inTransaction) { 323 sendInvalidationsToOthers(); 324 // as we don't have a way to know when the next 325 // non-transactional 326 // statement will start, process invalidations immediately 327 } 328 processReceivedInvalidations(); 329 } finally { 330 timerContext.stop(); 331 } 332 } 333 334 protected void flush() { 335 checkThread(); 336 List<Work> works; 337 if (!repository.getRepositoryDescriptor().getFulltextDescriptor().getFulltextDisabled()) { 338 works = getFulltextWorks(); 339 } else { 340 works = Collections.emptyList(); 341 } 342 doFlush(); 343 if (readAclsChanged) { 344 updateReadAcls(); 345 } 346 scheduleWork(works); 347 checkInvalidationsConflict(); 348 } 349 350 protected void scheduleWork(List<Work> works) { 351 // do async fulltext indexing only if high-level sessions are available 352 RepositoryManager repositoryManager = Framework.getService(RepositoryManager.class); 353 if (repositoryManager != null && !works.isEmpty()) { 354 WorkManager workManager = Framework.getService(WorkManager.class); 355 for (Work work : works) { 356 // schedule work post-commit 357 // in non-tx mode, this may execute it nearly immediately 358 workManager.schedule(work, Scheduling.IF_NOT_SCHEDULED, true); 359 } 360 } 361 } 362 363 protected void doFlush() { 364 List<Fragment> fragmentsToClearDirty = new ArrayList<>(0); 365 RowBatch batch = context.getSaveBatch(fragmentsToClearDirty); 366 if (!batch.isEmpty()) { 367 log.debug("Saving session"); 368 // execute the batch 369 mapper.write(batch); 370 log.debug("End of save"); 371 for (Fragment fragment : fragmentsToClearDirty) { 372 fragment.clearDirty(); 373 } 374 } 375 } 376 377 protected Serializable getContainingDocument(Serializable id) { 378 return context.getContainingDocument(id); 379 } 380 381 /** 382 * Gets the fulltext updates to do. Called at save() time. 383 * 384 * @return a list of {@link Work} instances to schedule post-commit. 385 */ 386 protected List<Work> getFulltextWorks() { 387 Set<Serializable> dirtyStrings = new HashSet<>(); 388 Set<Serializable> dirtyBinaries = new HashSet<>(); 389 context.findDirtyDocuments(dirtyStrings, dirtyBinaries); 390 if (dirtyStrings.isEmpty() && dirtyBinaries.isEmpty()) { 391 return Collections.emptyList(); 392 } 393 394 List<Work> works = new LinkedList<>(); 395 getFulltextSimpleWorks(works, dirtyStrings); 396 getFulltextBinariesWorks(works, dirtyBinaries); 397 return works; 398 } 399 400 protected void getFulltextSimpleWorks(List<Work> works, Set<Serializable> dirtyStrings) { 401 FulltextConfiguration fulltextConfiguration = model.getFulltextConfiguration(); 402 if (fulltextConfiguration.fulltextSearchDisabled) { 403 return; 404 } 405 // update simpletext on documents with dirty strings 406 for (Serializable docId : dirtyStrings) { 407 if (docId == null) { 408 // cannot happen, but has been observed :( 409 log.error("Got null doc id in fulltext update, cannot happen"); 410 continue; 411 } 412 Node document = getNodeById(docId); 413 if (document == null) { 414 // cannot happen 415 continue; 416 } 417 if (document.isProxy()) { 418 // proxies don't have any fulltext attached, it's 419 // the target document that carries it 420 continue; 421 } 422 String documentType = document.getPrimaryType(); 423 String[] mixinTypes = document.getMixinTypes(); 424 425 if (!fulltextConfiguration.isFulltextIndexable(documentType)) { 426 continue; 427 } 428 document.getSimpleProperty(Model.FULLTEXT_JOBID_PROP).setValue(model.idToString(document.getId())); 429 FulltextFinder fulltextFinder = new FulltextFinder(fulltextParser, document, this); 430 List<IndexAndText> indexesAndText = new LinkedList<>(); 431 for (String indexName : fulltextConfiguration.indexNames) { 432 Set<String> paths; 433 if (fulltextConfiguration.indexesAllSimple.contains(indexName)) { 434 // index all string fields, minus excluded ones 435 // TODO XXX excluded ones... 436 paths = model.getSimpleTextPropertyPaths(documentType, mixinTypes); 437 } else { 438 // index configured fields 439 paths = fulltextConfiguration.propPathsByIndexSimple.get(indexName); 440 } 441 String text = fulltextFinder.findFulltext(paths); 442 indexesAndText.add(new IndexAndText(indexName, text)); 443 } 444 if (!indexesAndText.isEmpty()) { 445 Work work = new FulltextUpdaterWork(repository.getName(), model.idToString(docId), true, false, 446 indexesAndText); 447 works.add(work); 448 } 449 } 450 } 451 452 protected void getFulltextBinariesWorks(List<Work> works, final Set<Serializable> dirtyBinaries) { 453 if (dirtyBinaries.isEmpty()) { 454 return; 455 } 456 457 // mark indexing in progress, so that future copies (including versions) 458 // will be indexed as well 459 for (Node node : getNodesByIds(new ArrayList<>(dirtyBinaries))) { 460 if (!model.getFulltextConfiguration().isFulltextIndexable(node.getPrimaryType())) { 461 continue; 462 } 463 node.getSimpleProperty(Model.FULLTEXT_JOBID_PROP).setValue(model.idToString(node.getId())); 464 } 465 466 // FulltextExtractorWork does fulltext extraction using converters 467 // and then schedules a FulltextUpdaterWork to write the results 468 // single-threaded 469 for (Serializable id : dirtyBinaries) { 470 String docId = model.idToString(id); 471 Work work = new SQLFulltextExtractorWork(repository.getName(), docId); 472 works.add(work); 473 } 474 } 475 476 /** 477 * Finds the fulltext in a document and sends it to a fulltext parser. 478 * 479 * @since 5.9.5 480 */ 481 protected static class FulltextFinder { 482 483 protected final FulltextParser fulltextParser; 484 485 protected final Node document; 486 487 protected final SessionImpl session; 488 489 protected final String documentType; 490 491 protected final String[] mixinTypes; 492 493 public FulltextFinder(FulltextParser fulltextParser, Node document, SessionImpl session) { 494 this.fulltextParser = fulltextParser; 495 this.document = document; 496 this.session = session; 497 if (document == null) { 498 documentType = null; 499 mixinTypes = null; 500 } else { // null in tests 501 documentType = document.getPrimaryType(); 502 mixinTypes = document.getMixinTypes(); 503 } 504 } 505 506 /** 507 * Parses the document for one index. 508 */ 509 protected String findFulltext(Set<String> paths) { 510 if (paths == null) { 511 return ""; 512 } 513 List<String> strings = new ArrayList<>(); 514 515 for (String path : paths) { 516 ModelProperty pi = session.getModel().getPathPropertyInfo(documentType, mixinTypes, path); 517 if (pi == null) { 518 continue; // doc type doesn't have this property 519 } 520 if (pi.propertyType != PropertyType.STRING && pi.propertyType != PropertyType.ARRAY_STRING) { 521 continue; 522 } 523 524 List<Node> nodes = new ArrayList<>(Collections.singleton(document)); 525 526 String[] names = path.split("/"); 527 for (int i = 0; i < names.length; i++) { 528 String name = names[i]; 529 if (i < names.length - 1) { 530 // traverse 531 List<Node> newNodes; 532 if ("*".equals(names[i + 1])) { 533 // traverse complex list 534 i++; 535 newNodes = new ArrayList<>(); 536 for (Node node : nodes) { 537 newNodes.addAll(session.getChildren(node, name, true)); 538 } 539 } else { 540 // traverse child 541 newNodes = new ArrayList<>(nodes.size()); 542 for (Node node : nodes) { 543 node = session.getChildNode(node, name, true); 544 if (node != null) { 545 newNodes.add(node); 546 } 547 } 548 } 549 nodes = newNodes; 550 } else { 551 // last path component: get value 552 for (Node node : nodes) { 553 if (pi.propertyType == PropertyType.STRING) { 554 String v = node.getSimpleProperty(name).getString(); 555 if (v != null) { 556 fulltextParser.parse(v, path, strings); 557 } 558 } else { /* ARRAY_STRING */ 559 for (Serializable v : node.getCollectionProperty(name).getValue()) { 560 if (v != null) { 561 fulltextParser.parse((String) v, path, strings); 562 } 563 } 564 } 565 } 566 } 567 } 568 } 569 return StringUtils.join(strings, ' '); 570 } 571 } 572 573 /** 574 * Post-transaction invalidations notification. 575 * <p> 576 * Called post-transaction by session commit/rollback or transactionless save. 577 */ 578 protected void sendInvalidationsToOthers() { 579 context.sendInvalidationsToOthers(); 580 } 581 582 /** 583 * Processes all invalidations accumulated. 584 * <p> 585 * Called pre-transaction by start or transactionless save; 586 */ 587 protected void processReceivedInvalidations() { 588 context.processReceivedInvalidations(); 589 } 590 591 /** 592 * Post transaction check invalidations processing. 593 */ 594 protected void checkInvalidationsConflict() { 595 // repository.receiveClusterInvalidations(this); 596 context.checkInvalidationsConflict(); 597 } 598 599 /* 600 * ------------------------------------------------------------- 601 * ------------------------------------------------------------- 602 * ------------------------------------------------------------- 603 */ 604 605 protected Node getNodeById(Serializable id, boolean prefetch) { 606 List<Node> nodes = getNodesByIds(Collections.singletonList(id), prefetch); 607 Node node = nodes.get(0); 608 // ((JDBCMapper) ((CachingMapper) 609 // mapper).mapper).logger.log("getNodeById " + id + " -> " + (node == 610 // null ? "missing" : "found")); 611 return node; 612 } 613 614 @Override 615 public Node getNodeById(Serializable id) { 616 checkLive(); 617 if (id == null) { 618 throw new IllegalArgumentException("Illegal null id"); 619 } 620 return getNodeById(id, true); 621 } 622 623 public List<Node> getNodesByIds(List<Serializable> ids, boolean prefetch) { 624 // get hier fragments 625 List<RowId> hierRowIds = new ArrayList<>(ids.size()); 626 for (Serializable id : ids) { 627 hierRowIds.add(new RowId(Model.HIER_TABLE_NAME, id)); 628 } 629 630 List<Fragment> hierFragments = context.getMulti(hierRowIds, false); 631 632 // find available paths 633 Map<Serializable, String> paths = new HashMap<>(); 634 Set<Serializable> parentIds = new HashSet<>(); 635 for (Fragment fragment : hierFragments) { 636 Serializable id = fragment.getId(); 637 PathAndId pathOrId = context.getPathOrMissingParentId((SimpleFragment) fragment, false); 638 // find missing fragments 639 if (pathOrId.path != null) { 640 paths.put(id, pathOrId.path); 641 } else { 642 parentIds.add(pathOrId.id); 643 } 644 } 645 // fetch the missing parents and their ancestors in bulk 646 if (!parentIds.isEmpty()) { 647 // fetch them in the context 648 getHierarchyAndAncestors(parentIds); 649 // compute missing paths using context 650 for (Fragment fragment : hierFragments) { 651 Serializable id = fragment.getId(); 652 if (paths.containsKey(id)) { 653 continue; 654 } 655 String path = context.getPath((SimpleFragment) fragment); 656 paths.put(id, path); 657 } 658 } 659 660 // prepare fragment groups to build nodes 661 Map<Serializable, FragmentGroup> fragmentGroups = new HashMap<>(ids.size()); 662 for (Fragment fragment : hierFragments) { 663 Serializable id = fragment.row.id; 664 fragmentGroups.put(id, new FragmentGroup((SimpleFragment) fragment, new FragmentsMap())); 665 } 666 667 if (prefetch) { 668 List<RowId> bulkRowIds = new ArrayList<>(); 669 Set<Serializable> proxyIds = new HashSet<>(); 670 671 // get rows to prefetch for hier fragments 672 for (Fragment fragment : hierFragments) { 673 findPrefetchedFragments((SimpleFragment) fragment, bulkRowIds, proxyIds); 674 } 675 676 // proxies 677 678 // get proxies fragments 679 List<RowId> proxiesRowIds = new ArrayList<>(proxyIds.size()); 680 for (Serializable id : proxyIds) { 681 proxiesRowIds.add(new RowId(Model.PROXY_TABLE_NAME, id)); 682 } 683 List<Fragment> proxiesFragments = context.getMulti(proxiesRowIds, true); 684 Set<Serializable> targetIds = new HashSet<>(); 685 for (Fragment fragment : proxiesFragments) { 686 Serializable targetId = ((SimpleFragment) fragment).get(Model.PROXY_TARGET_KEY); 687 targetIds.add(targetId); 688 } 689 690 // get hier fragments for proxies' targets 691 targetIds.removeAll(ids); // only those we don't have already 692 hierRowIds = new ArrayList<>(targetIds.size()); 693 for (Serializable id : targetIds) { 694 hierRowIds.add(new RowId(Model.HIER_TABLE_NAME, id)); 695 } 696 hierFragments = context.getMulti(hierRowIds, true); 697 for (Fragment fragment : hierFragments) { 698 findPrefetchedFragments((SimpleFragment) fragment, bulkRowIds, null); 699 } 700 701 // we have everything to be prefetched 702 703 // fetch all the prefetches in bulk 704 List<Fragment> fragments = context.getMulti(bulkRowIds, true); 705 706 // put each fragment in the map of the proper group 707 for (Fragment fragment : fragments) { 708 FragmentGroup fragmentGroup = fragmentGroups.get(fragment.row.id); 709 if (fragmentGroup != null) { 710 fragmentGroup.fragments.put(fragment.row.tableName, fragment); 711 } 712 } 713 } 714 715 // assemble nodes from the fragment groups 716 List<Node> nodes = new ArrayList<>(ids.size()); 717 for (Serializable id : ids) { 718 FragmentGroup fragmentGroup = fragmentGroups.get(id); 719 // null if deleted/absent 720 Node node = fragmentGroup == null ? null : new Node(context, fragmentGroup, paths.get(id)); 721 nodes.add(node); 722 } 723 724 return nodes; 725 } 726 727 /** 728 * Finds prefetched fragments for a hierarchy fragment, takes note of the ones that are proxies. 729 */ 730 protected void findPrefetchedFragments(SimpleFragment hierFragment, List<RowId> bulkRowIds, 731 Set<Serializable> proxyIds) { 732 Serializable id = hierFragment.row.id; 733 734 // find type 735 String typeName = (String) hierFragment.get(Model.MAIN_PRIMARY_TYPE_KEY); 736 if (Model.PROXY_TYPE.equals(typeName)) { 737 if (proxyIds != null) { 738 proxyIds.add(id); 739 } 740 return; 741 } 742 743 // find table names 744 Set<String> tableNames = model.getTypePrefetchedFragments(typeName); 745 if (tableNames == null) { 746 return; // unknown (obsolete) type 747 } 748 749 // add row id for each table name 750 Serializable parentId = hierFragment.get(Model.HIER_PARENT_KEY); 751 for (String tableName : tableNames) { 752 if (Model.HIER_TABLE_NAME.equals(tableName)) { 753 continue; // already fetched 754 } 755 if (parentId != null && Model.VERSION_TABLE_NAME.equals(tableName)) { 756 continue; // not a version, don't fetch this table 757 // TODO incorrect if we have filed versions 758 } 759 bulkRowIds.add(new RowId(tableName, id)); 760 } 761 } 762 763 @Override 764 public List<Node> getNodesByIds(List<Serializable> ids) { 765 checkLive(); 766 return getNodesByIds(ids, true); 767 } 768 769 @Override 770 public Node getParentNode(Node node) { 771 checkLive(); 772 if (node == null) { 773 throw new IllegalArgumentException("Illegal null node"); 774 } 775 Serializable id = node.getHierFragment().get(Model.HIER_PARENT_KEY); 776 return id == null ? null : getNodeById(id); 777 } 778 779 @Override 780 public String getPath(Node node) { 781 checkLive(); 782 String path = node.getPath(); 783 if (path == null) { 784 path = context.getPath(node.getHierFragment()); 785 } 786 return path; 787 } 788 789 /* 790 * Normalize using NFC to avoid decomposed characters (like 'e' + COMBINING ACUTE ACCENT instead of LATIN SMALL 791 * LETTER E WITH ACUTE). NFKC (normalization using compatibility decomposition) is not used, because compatibility 792 * decomposition turns some characters (LATIN SMALL LIGATURE FFI, TRADE MARK SIGN, FULLWIDTH SOLIDUS) into a series 793 * of characters ('f'+'f'+'i', 'T'+'M', '/') that cannot be re-composed into the original, and therefore loses 794 * information. 795 */ 796 protected String normalize(String path) { 797 return Normalizer.normalize(path, Normalizer.Form.NFC); 798 } 799 800 /* Does not apply to properties for now (no use case). */ 801 @Override 802 public Node getNodeByPath(String path, Node node) { 803 // TODO optimize this to use a dedicated path-based table 804 checkLive(); 805 if (path == null) { 806 throw new IllegalArgumentException("Illegal null path"); 807 } 808 path = normalize(path); 809 int i; 810 if (path.startsWith("/")) { 811 node = getRootNode(); 812 if (path.equals("/")) { 813 return node; 814 } 815 i = 1; 816 } else { 817 if (node == null) { 818 throw new IllegalArgumentException("Illegal relative path with null node: " + path); 819 } 820 i = 0; 821 } 822 String[] names = path.split("/", -1); 823 for (; i < names.length; i++) { 824 String name = names[i]; 825 if (name.length() == 0) { 826 throw new IllegalArgumentException("Illegal path with empty component: " + path); 827 } 828 node = getChildNode(node, name, false); 829 if (node == null) { 830 return null; 831 } 832 } 833 return node; 834 } 835 836 @Override 837 public boolean addMixinType(Node node, String mixin) { 838 if (model.getMixinPropertyInfos(mixin) == null) { 839 throw new IllegalArgumentException("No such mixin: " + mixin); 840 } 841 if (model.getDocumentTypeFacets(node.getPrimaryType()).contains(mixin)) { 842 return false; // already present in type 843 } 844 List<String> list = new ArrayList<>(Arrays.asList(node.getMixinTypes())); 845 if (list.contains(mixin)) { 846 return false; // already present in node 847 } 848 Set<String> otherChildrenNames = getChildrenNames(node.getPrimaryType(), list); 849 list.add(mixin); 850 String[] mixins = list.toArray(new String[list.size()]); 851 node.hierFragment.put(Model.MAIN_MIXIN_TYPES_KEY, mixins); 852 // immediately create child nodes (for complex properties) in order 853 // to avoid concurrency issue later on 854 Map<String, String> childrenTypes = model.getMixinComplexChildren(mixin); 855 for (Entry<String, String> es : childrenTypes.entrySet()) { 856 String childName = es.getKey(); 857 String childType = es.getValue(); 858 // child may already exist if the schema is part of the primary type or another facet 859 if (otherChildrenNames.contains(childName)) { 860 continue; 861 } 862 addChildNode(node, childName, null, childType, true); 863 } 864 return true; 865 } 866 867 @Override 868 public boolean removeMixinType(Node node, String mixin) { 869 List<String> list = new ArrayList<>(Arrays.asList(node.getMixinTypes())); 870 if (!list.remove(mixin)) { 871 return false; // not present in node 872 } 873 String[] mixins = list.toArray(new String[list.size()]); 874 if (mixins.length == 0) { 875 mixins = null; 876 } 877 node.hierFragment.put(Model.MAIN_MIXIN_TYPES_KEY, mixins); 878 Set<String> otherChildrenNames = getChildrenNames(node.getPrimaryType(), list); 879 Map<String, String> childrenTypes = model.getMixinComplexChildren(mixin); 880 for (String childName : childrenTypes.keySet()) { 881 // child must be kept if the schema is part of primary type or another facet 882 if (otherChildrenNames.contains(childName)) { 883 continue; 884 } 885 Node child = getChildNode(node, childName, true); 886 removePropertyNode(child); 887 } 888 node.clearCache(); 889 return true; 890 } 891 892 @Override 893 public ScrollResult<String> scroll(String query, int batchSize, int keepAliveSeconds) { 894 return mapper.scroll(query, batchSize, keepAliveSeconds); 895 } 896 897 @Override 898 public ScrollResult<String> scroll(String scrollId) { 899 return mapper.scroll(scrollId); 900 } 901 902 /** 903 * Gets complex children names defined by the primary type and the list of mixins. 904 */ 905 protected Set<String> getChildrenNames(String primaryType, List<String> mixins) { 906 Map<String, String> cc = model.getTypeComplexChildren(primaryType); 907 if (cc == null) { 908 cc = Collections.emptyMap(); 909 } 910 Set<String> childrenNames = new HashSet<>(cc.keySet()); 911 for (String mixin : mixins) { 912 cc = model.getMixinComplexChildren(mixin); 913 if (cc != null) { 914 childrenNames.addAll(cc.keySet()); 915 } 916 } 917 return childrenNames; 918 } 919 920 @Override 921 public Node addChildNode(Node parent, String name, Long pos, String typeName, boolean complexProp) { 922 if (pos == null && !complexProp && parent != null) { 923 pos = context.getNextPos(parent.getId(), complexProp); 924 } 925 return addChildNode(null, parent, name, pos, typeName, complexProp); 926 } 927 928 @Override 929 public Node addChildNode(Serializable id, Node parent, String name, Long pos, String typeName, 930 boolean complexProp) { 931 checkLive(); 932 if (name == null) { 933 throw new IllegalArgumentException("Illegal null name"); 934 } 935 name = normalize(name); 936 if (name.contains("/") || name.equals(".") || name.equals("..")) { 937 throw new IllegalArgumentException("Illegal name: " + name); 938 } 939 if (!model.isType(typeName)) { 940 throw new IllegalArgumentException("Unknown type: " + typeName); 941 } 942 id = generateNewId(id); 943 Serializable parentId = parent == null ? null : parent.hierFragment.getId(); 944 Node node = addNode(id, parentId, name, pos, typeName, complexProp); 945 // immediately create child nodes (for complex properties) in order 946 // to avoid concurrency issue later on 947 Map<String, String> childrenTypes = model.getTypeComplexChildren(typeName); 948 for (Entry<String, String> es : childrenTypes.entrySet()) { 949 String childName = es.getKey(); 950 String childType = es.getValue(); 951 addChildNode(node, childName, null, childType, true); 952 } 953 return node; 954 } 955 956 protected Node addNode(Serializable id, Serializable parentId, String name, Long pos, String typeName, 957 boolean complexProp) { 958 requireReadAclsUpdate(); 959 // main info 960 Row hierRow = new Row(Model.HIER_TABLE_NAME, id); 961 hierRow.putNew(Model.HIER_PARENT_KEY, parentId); 962 hierRow.putNew(Model.HIER_CHILD_NAME_KEY, name); 963 hierRow.putNew(Model.HIER_CHILD_POS_KEY, pos); 964 hierRow.putNew(Model.MAIN_PRIMARY_TYPE_KEY, typeName); 965 hierRow.putNew(Model.HIER_CHILD_ISPROPERTY_KEY, Boolean.valueOf(complexProp)); 966 if (changeTokenEnabled) { 967 hierRow.putNew(Model.MAIN_SYS_CHANGE_TOKEN_KEY, Model.INITIAL_SYS_CHANGE_TOKEN); 968 } 969 SimpleFragment hierFragment = context.createHierarchyFragment(hierRow); 970 FragmentGroup fragmentGroup = new FragmentGroup(hierFragment, new FragmentsMap()); 971 return new Node(context, fragmentGroup, context.getPath(hierFragment)); 972 } 973 974 @Override 975 public Node addProxy(Serializable targetId, Serializable versionableId, Node parent, String name, Long pos) { 976 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 977 throw new NuxeoException("Proxies are disabled by configuration"); 978 } 979 Node proxy = addChildNode(parent, name, pos, Model.PROXY_TYPE, false); 980 proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId); 981 proxy.setSimpleProperty(Model.PROXY_VERSIONABLE_PROP, versionableId); 982 if (changeTokenEnabled) { 983 proxy.setSimpleProperty(Model.MAIN_SYS_CHANGE_TOKEN_PROP, Model.INITIAL_SYS_CHANGE_TOKEN); 984 proxy.setSimpleProperty(Model.MAIN_CHANGE_TOKEN_PROP, Model.INITIAL_CHANGE_TOKEN); 985 } 986 SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME); 987 context.createdProxyFragment(proxyFragment); 988 return proxy; 989 } 990 991 @Override 992 public void setProxyTarget(Node proxy, Serializable targetId) { 993 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 994 throw new NuxeoException("Proxies are disabled by configuration"); 995 } 996 SimpleProperty prop = proxy.getSimpleProperty(Model.PROXY_TARGET_PROP); 997 Serializable oldTargetId = prop.getValue(); 998 if (!oldTargetId.equals(targetId)) { 999 SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME); 1000 context.removedProxyTarget(proxyFragment); 1001 proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId); 1002 context.addedProxyTarget(proxyFragment); 1003 } 1004 } 1005 1006 @Override 1007 public boolean hasChildNode(Node parent, String name, boolean complexProp) { 1008 checkLive(); 1009 // TODO could optimize further by not fetching the fragment at all 1010 SimpleFragment fragment = context.getChildHierByName(parent.getId(), normalize(name), complexProp); 1011 return fragment != null; 1012 } 1013 1014 @Override 1015 public Node getChildNode(Node parent, String name, boolean complexProp) { 1016 checkLive(); 1017 if (name == null || name.contains("/") || name.equals(".") || name.equals("..")) { 1018 throw new IllegalArgumentException("Illegal name: " + name); 1019 } 1020 SimpleFragment fragment = context.getChildHierByName(parent.getId(), name, complexProp); 1021 return fragment == null ? null : getNodeById(fragment.getId()); 1022 } 1023 1024 // TODO optimize with dedicated backend call 1025 @Override 1026 public boolean hasChildren(Node parent, boolean complexProp) { 1027 checkLive(); 1028 List<SimpleFragment> children = context.getChildren(parent.getId(), null, complexProp); 1029 if (complexProp) { 1030 return !children.isEmpty(); 1031 } 1032 if (children.isEmpty()) { 1033 return false; 1034 } 1035 // we have to check that type names are not obsolete, as they wouldn't be returned 1036 // by getChildren and we must be consistent 1037 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 1038 for (SimpleFragment simpleFragment : children) { 1039 String primaryType = simpleFragment.getString(Model.MAIN_PRIMARY_TYPE_KEY); 1040 if (primaryType.equals(Model.PROXY_TYPE)) { 1041 Node node = getNodeById(simpleFragment.getId(), false); 1042 Serializable targetId = node.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue(); 1043 if (targetId == null) { 1044 // missing target, should not happen, ignore 1045 continue; 1046 } 1047 Node target = getNodeById(targetId, false); 1048 if (target == null) { 1049 continue; 1050 } 1051 primaryType = target.getPrimaryType(); 1052 } 1053 DocumentType type = schemaManager.getDocumentType(primaryType); 1054 if (type == null) { 1055 // obsolete type, ignored in getChildren 1056 continue; 1057 } 1058 return true; 1059 } 1060 return false; 1061 } 1062 1063 @Override 1064 public List<Node> getChildren(Node parent, String name, boolean complexProp) { 1065 checkLive(); 1066 List<SimpleFragment> fragments = context.getChildren(parent.getId(), name, complexProp); 1067 List<Node> nodes = new ArrayList<>(fragments.size()); 1068 for (SimpleFragment fragment : fragments) { 1069 Node node = getNodeById(fragment.getId()); 1070 if (node == null) { 1071 // cannot happen 1072 log.error("Child node cannot be created: " + fragment.getId()); 1073 continue; 1074 } 1075 nodes.add(node); 1076 } 1077 return nodes; 1078 } 1079 1080 @Override 1081 public void orderBefore(Node parent, Node source, Node dest) { 1082 checkLive(); 1083 context.orderBefore(parent.getId(), source.getId(), dest == null ? null : dest.getId()); 1084 } 1085 1086 @Override 1087 public Node move(Node source, Node parent, String name) { 1088 checkLive(); 1089 if (!parent.getId().equals(source.getParentId())) { 1090 flush(); // needed when doing many moves for circular stuff 1091 } 1092 context.move(source, parent.getId(), name); 1093 requireReadAclsUpdate(); 1094 return source; 1095 } 1096 1097 @Override 1098 public Node copy(Node source, Node parent, String name) { 1099 checkLive(); 1100 flush(); 1101 Serializable id = context.copy(source, parent.getId(), name); 1102 requireReadAclsUpdate(); 1103 return getNodeById(id); 1104 } 1105 1106 @Override 1107 public void removeNode(Node node) { 1108 checkLive(); 1109 flush(); 1110 // remove the lock using the lock manager 1111 // TODO children locks? 1112 Serializable id = node.getId(); 1113 getLockManager().removeLock(model.idToString(id), null); 1114 // find all descendants 1115 List<NodeInfo> nodeInfos = context.getNodeAndDescendantsInfo(node.getHierFragment()); 1116 1117 // check that there is no active retention 1118 Set<Serializable> retentionActiveIds = nodeInfos.stream() // 1119 .filter(info -> info.isRetentionActive) 1120 .map(info -> info.id) 1121 .collect(Collectors.toSet()); 1122 if (!retentionActiveIds.isEmpty()) { 1123 if (retentionActiveIds.contains(id)) { 1124 throw new DocumentExistsException( 1125 "Cannot remove " + id + ", it is under active retention"); 1126 } else { 1127 throw new DocumentExistsException("Cannot remove " + id + ", subdocument " 1128 + retentionActiveIds.iterator().next() + " is under active retention"); 1129 } 1130 } 1131 1132 // if a proxy target is removed, check that all proxies to it are removed 1133 if (repository.getRepositoryDescriptor().getProxiesEnabled()) { 1134 Set<Serializable> removedIds = nodeInfos.stream().map(info -> info.id).collect(Collectors.toSet()); 1135 // find proxies pointing to any removed document 1136 Set<Serializable> proxyIds = context.getTargetProxies(removedIds); 1137 for (Serializable proxyId : proxyIds) { 1138 if (!removedIds.contains(proxyId)) { 1139 Node proxy = getNodeById(proxyId); 1140 Serializable targetId = (Serializable) proxy.getSingle(Model.PROXY_TARGET_PROP); 1141 throw new DocumentExistsException( 1142 "Cannot remove " + id + ", subdocument " + targetId + " is the target of proxy " + proxyId); 1143 } 1144 } 1145 } 1146 1147 // remove all nodes 1148 context.removeNode(node.getHierFragment(), nodeInfos); 1149 } 1150 1151 @Override 1152 public void removePropertyNode(Node node) { 1153 checkLive(); 1154 // no flush needed 1155 context.removePropertyNode(node.getHierFragment()); 1156 } 1157 1158 @Override 1159 public Node checkIn(Node node, String label, String checkinComment) { 1160 checkLive(); 1161 flush(); 1162 Serializable id = context.checkIn(node, label, checkinComment); 1163 requireReadAclsUpdate(); 1164 // save to reflect changes immediately in database 1165 flush(); 1166 return getNodeById(id); 1167 } 1168 1169 @Override 1170 public void checkOut(Node node) { 1171 checkLive(); 1172 context.checkOut(node); 1173 requireReadAclsUpdate(); 1174 } 1175 1176 @Override 1177 public void restore(Node node, Node version) { 1178 checkLive(); 1179 // save done inside method 1180 context.restoreVersion(node, version); 1181 requireReadAclsUpdate(); 1182 } 1183 1184 @Override 1185 public Node getVersionByLabel(Serializable versionSeriesId, String label) { 1186 if (label == null) { 1187 return null; 1188 } 1189 List<Node> versions = getVersions(versionSeriesId); 1190 for (Node node : versions) { 1191 String l = (String) node.getSimpleProperty(Model.VERSION_LABEL_PROP).getValue(); 1192 if (label.equals(l)) { 1193 return node; 1194 } 1195 } 1196 return null; 1197 } 1198 1199 @Override 1200 public Node getLastVersion(Serializable versionSeriesId) { 1201 checkLive(); 1202 List<Serializable> ids = context.getVersionIds(versionSeriesId); 1203 return ids.isEmpty() ? null : getNodeById(ids.get(ids.size() - 1)); 1204 } 1205 1206 @Override 1207 public List<Node> getVersions(Serializable versionSeriesId) { 1208 checkLive(); 1209 List<Serializable> ids = context.getVersionIds(versionSeriesId); 1210 List<Node> nodes = new ArrayList<>(ids.size()); 1211 for (Serializable id : ids) { 1212 nodes.add(getNodeById(id)); 1213 } 1214 return nodes; 1215 } 1216 1217 @Override 1218 public List<Node> getProxies(Node document, Node parent) { 1219 checkLive(); 1220 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 1221 return Collections.emptyList(); 1222 } 1223 1224 List<Serializable> ids; 1225 if (document.isVersion()) { 1226 ids = context.getTargetProxyIds(document.getId()); 1227 } else { 1228 Serializable versionSeriesId; 1229 if (document.isProxy()) { 1230 versionSeriesId = document.getSimpleProperty(Model.PROXY_VERSIONABLE_PROP).getValue(); 1231 } else { 1232 versionSeriesId = document.getId(); 1233 } 1234 ids = context.getSeriesProxyIds(versionSeriesId); 1235 } 1236 1237 List<Node> nodes = new LinkedList<>(); 1238 for (Serializable id : ids) { 1239 Node node = getNodeById(id); 1240 if (node != null || Boolean.TRUE.booleanValue()) { // XXX 1241 // null if deleted, which means selection wasn't correctly 1242 // updated 1243 nodes.add(node); 1244 } 1245 } 1246 1247 if (parent != null) { 1248 // filter by parent 1249 Serializable parentId = parent.getId(); 1250 for (Iterator<Node> it = nodes.iterator(); it.hasNext();) { 1251 Node node = it.next(); 1252 if (!parentId.equals(node.getParentId())) { 1253 it.remove(); 1254 } 1255 } 1256 } 1257 1258 return nodes; 1259 } 1260 1261 /** 1262 * Fetches the hierarchy fragment for the given rows and all their ancestors. 1263 * 1264 * @param ids the fragment ids 1265 */ 1266 protected List<Fragment> getHierarchyAndAncestors(Collection<Serializable> ids) { 1267 Set<Serializable> allIds = mapper.getAncestorsIds(ids); 1268 allIds.addAll(ids); 1269 List<RowId> rowIds = new ArrayList<>(allIds.size()); 1270 for (Serializable id : allIds) { 1271 rowIds.add(new RowId(Model.HIER_TABLE_NAME, id)); 1272 } 1273 return context.getMulti(rowIds, true); 1274 } 1275 1276 @Override 1277 public PartialList<Serializable> query(String query, QueryFilter queryFilter, boolean countTotal) { 1278 final Timer.Context timerContext = queryTimer.time(); 1279 try { 1280 return mapper.query(query, NXQL.NXQL, queryFilter, countTotal); 1281 } finally { 1282 timerContext.stop(); 1283 } 1284 } 1285 1286 @Override 1287 public PartialList<Serializable> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) { 1288 final Timer.Context timerContext = queryTimer.time(); 1289 try { 1290 return mapper.query(query, queryType, queryFilter, countUpTo); 1291 } finally { 1292 long duration = timerContext.stop(); 1293 if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) { 1294 String msg = String.format("duration_ms:\t%.2f\t%s %s\tquery\t%s", duration / 1000000.0, queryFilter, 1295 countUpToAsString(countUpTo), query); 1296 if (log.isTraceEnabled()) { 1297 log.info(msg, new Throwable("Slow query stack trace")); 1298 } else { 1299 log.info(msg); 1300 } 1301 } 1302 } 1303 } 1304 1305 private String countUpToAsString(long countUpTo) { 1306 if (countUpTo > 0) { 1307 return String.format("count total results up to %d", countUpTo); 1308 } 1309 return countUpTo == -1 ? "count total results UNLIMITED" : ""; 1310 } 1311 1312 @Override 1313 public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, 1314 Object... params) { 1315 return queryAndFetch(query, queryType, queryFilter, false, params); 1316 } 1317 1318 @Override 1319 public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, 1320 boolean distinctDocuments, Object... params) { 1321 final Timer.Context timerContext = queryTimer.time(); 1322 try { 1323 return mapper.queryAndFetch(query, queryType, queryFilter, distinctDocuments, params); 1324 } finally { 1325 long duration = timerContext.stop(); 1326 if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) { 1327 String msg = String.format("duration_ms:\t%.2f\t%s\tqueryAndFetch\t%s", duration / 1000000.0, 1328 queryFilter, query); 1329 if (log.isTraceEnabled()) { 1330 log.info(msg, new Throwable("Slow query stack trace")); 1331 } else { 1332 log.info(msg); 1333 } 1334 } 1335 } 1336 } 1337 1338 @Override 1339 public PartialList<Map<String, Serializable>> queryProjection(String query, String queryType, 1340 QueryFilter queryFilter, boolean distinctDocuments, long countUpTo, Object... params) { 1341 final Timer.Context timerContext = queryTimer.time(); 1342 try { 1343 return mapper.queryProjection(query, queryType, queryFilter, distinctDocuments, countUpTo, params); 1344 } finally { 1345 long duration = timerContext.stop(); 1346 if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) { 1347 String msg = String.format("duration_ms:\t%.2f\t%s\tqueryProjection\t%s", duration / 1000000.0, 1348 queryFilter, query); 1349 if (log.isTraceEnabled()) { 1350 log.info(msg, new Throwable("Slow query stack trace")); 1351 } else { 1352 log.info(msg); 1353 } 1354 } 1355 } 1356 } 1357 1358 @Override 1359 public LockManager getLockManager() { 1360 return repository.getLockManager(); 1361 } 1362 1363 @Override 1364 public void requireReadAclsUpdate() { 1365 readAclsChanged = true; 1366 } 1367 1368 @Override 1369 public void updateReadAcls() { 1370 final Timer.Context timerContext = aclrUpdateTimer.time(); 1371 try { 1372 mapper.updateReadAcls(); 1373 readAclsChanged = false; 1374 } finally { 1375 timerContext.stop(); 1376 } 1377 } 1378 1379 @Override 1380 public void rebuildReadAcls() { 1381 mapper.rebuildReadAcls(); 1382 readAclsChanged = false; 1383 } 1384 1385 private void computeRootNode() { 1386 String repositoryId = repository.getName(); 1387 Serializable rootId = mapper.getRootId(repositoryId); 1388 if (rootId == null && COMPAT_REPOSITORY_NAME) { 1389 // compat, old repositories had fixed id "default" 1390 rootId = mapper.getRootId("default"); 1391 } 1392 if (rootId == null) { 1393 log.debug("Creating root"); 1394 rootNode = addRootNode(); 1395 addRootACP(); 1396 save(); 1397 // record information about the root id 1398 mapper.setRootId(repositoryId, rootNode.getId()); 1399 } else { 1400 rootNode = getNodeById(rootId, false); 1401 } 1402 } 1403 1404 // TODO factor with addChildNode 1405 private Node addRootNode() { 1406 Serializable id = generateNewId(null); 1407 return addNode(id, null, "", null, Model.ROOT_TYPE, false); 1408 } 1409 1410 private void addRootACP() { 1411 ACLRow[] aclrows = new ACLRow[3]; 1412 // TODO put groups in their proper place. like that now for consistency. 1413 aclrows[0] = new ACLRow(0, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATORS, 1414 null); 1415 aclrows[1] = new ACLRow(1, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATOR, 1416 null); 1417 aclrows[2] = new ACLRow(2, ACL.LOCAL_ACL, true, SecurityConstants.READ, SecurityConstants.MEMBERS, null); 1418 rootNode.setCollectionProperty(Model.ACL_PROP, aclrows); 1419 requireReadAclsUpdate(); 1420 } 1421 1422 // public Node newNodeInstance() needed ? 1423 1424 public void checkPermission(String absPath, String actions) { 1425 checkLive(); 1426 // TODO Auto-generated method stub 1427 throw new RuntimeException("Not implemented"); 1428 } 1429 1430 public boolean hasPendingChanges() { 1431 checkLive(); 1432 // TODO Auto-generated method stub 1433 throw new RuntimeException("Not implemented"); 1434 } 1435 1436 public void markReferencedBinaries() { 1437 checkLive(); 1438 mapper.markReferencedBinaries(); 1439 } 1440 1441 public int cleanupDeletedDocuments(int max, Calendar beforeTime) { 1442 checkLive(); 1443 if (!repository.getRepositoryDescriptor().getSoftDeleteEnabled()) { 1444 return 0; 1445 } 1446 return mapper.cleanupDeletedRows(max, beforeTime); 1447 } 1448 1449 /* 1450 * ----- XAResource ----- 1451 */ 1452 1453 @Override 1454 public boolean isSameRM(XAResource xaresource) { 1455 return xaresource == this; 1456 } 1457 1458 @Override 1459 public void start(Xid xid, int flags) throws XAException { 1460 if (flags == TMNOFLAGS) { 1461 try { 1462 processReceivedInvalidations(); 1463 } catch (NuxeoException e) { 1464 log.error("Could not start transaction", e); 1465 throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e); 1466 } 1467 } 1468 mapper.start(xid, flags); 1469 inTransaction = true; 1470 checkThreadStart(); 1471 } 1472 1473 @Override 1474 public void end(Xid xid, int flags) throws XAException { 1475 boolean failed = true; 1476 try { 1477 if (flags != TMFAIL) { 1478 try { 1479 flush(); 1480 } catch (ConcurrentUpdateException e) { 1481 TransactionHelper.noteSuppressedException(e); 1482 log.debug("Exception during transaction commit", e); 1483 // set rollback only manually instead of throwing, this avoids 1484 // a spurious log in Geronimo TransactionImpl and has the same effect 1485 TransactionHelper.setTransactionRollbackOnly(); 1486 return; 1487 } catch (NuxeoException e) { 1488 log.error("Exception during transaction commit", e); 1489 throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e); 1490 } 1491 } 1492 failed = false; 1493 mapper.end(xid, flags); 1494 } finally { 1495 if (failed) { 1496 mapper.end(xid, TMFAIL); 1497 // rollback done by tx manager 1498 } 1499 } 1500 } 1501 1502 @Override 1503 public int prepare(Xid xid) throws XAException { 1504 int res = mapper.prepare(xid); 1505 if (res == XA_RDONLY) { 1506 // Read-only optimization, commit() won't be called by the TM. 1507 // It's important to nevertheless send invalidations because 1508 // Oracle, in tightly-coupled transaction mode, can return 1509 // this status even when some changes were actually made 1510 // (they just will be committed by another resource). 1511 // See NXP-7943 1512 commitDone(); 1513 } 1514 return res; 1515 } 1516 1517 @Override 1518 public void commit(Xid xid, boolean onePhase) throws XAException { 1519 try { 1520 mapper.commit(xid, onePhase); 1521 } finally { 1522 commitDone(); 1523 } 1524 } 1525 1526 protected void commitDone() throws XAException { 1527 inTransaction = false; 1528 try { 1529 try { 1530 sendInvalidationsToOthers(); 1531 } finally { 1532 checkThreadEnd(); 1533 } 1534 } catch (NuxeoException e) { 1535 log.error("Could not send invalidations", e); 1536 throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e); 1537 } 1538 } 1539 1540 @Override 1541 public void rollback(Xid xid) throws XAException { 1542 try { 1543 try { 1544 mapper.rollback(xid); 1545 } finally { 1546 rollback(); 1547 } 1548 } finally { 1549 inTransaction = false; 1550 // no invalidations to send 1551 checkThreadEnd(); 1552 } 1553 } 1554 1555 @Override 1556 public void forget(Xid xid) throws XAException { 1557 mapper.forget(xid); 1558 } 1559 1560 @Override 1561 public Xid[] recover(int flag) throws XAException { 1562 return mapper.recover(flag); 1563 } 1564 1565 @Override 1566 public boolean setTransactionTimeout(int seconds) throws XAException { 1567 return mapper.setTransactionTimeout(seconds); 1568 } 1569 1570 @Override 1571 public int getTransactionTimeout() throws XAException { 1572 return mapper.getTransactionTimeout(); 1573 } 1574 1575 public long getCacheSize() { 1576 return context.getCacheSize(); 1577 } 1578 1579 public long getCacheMapperSize() { 1580 return context.getCacheMapperSize(); 1581 } 1582 1583 public long getCachePristineSize() { 1584 return context.getCachePristineSize(); 1585 } 1586 1587 public long getCacheSelectionSize() { 1588 return context.getCacheSelectionSize(); 1589 } 1590 1591 @Override 1592 public Map<String, String> getBinaryFulltext(Serializable id) { 1593 if (repository.getRepositoryDescriptor().getFulltextDescriptor().getFulltextDisabled()) { 1594 return null; 1595 } 1596 RowId rowId = new RowId(Model.FULLTEXT_TABLE_NAME, id); 1597 return mapper.getBinaryFulltext(rowId); 1598 } 1599 1600 @Override 1601 public boolean isChangeTokenEnabled() { 1602 return changeTokenEnabled; 1603 } 1604 1605 @Override 1606 public void markUserChange(Serializable id) { 1607 context.markUserChange(id); 1608 } 1609 1610}