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