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