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