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