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.FulltextParser; 062import org.nuxeo.ecm.core.storage.FulltextUpdaterWork; 063import org.nuxeo.ecm.core.storage.FulltextUpdaterWork.IndexAndText; 064import org.nuxeo.ecm.core.storage.sql.PersistenceContext.PathAndId; 065import org.nuxeo.ecm.core.storage.sql.RowMapper.RowBatch; 066import org.nuxeo.ecm.core.storage.sql.coremodel.SQLFulltextExtractorWork; 067import org.nuxeo.ecm.core.work.api.Work; 068import org.nuxeo.ecm.core.work.api.WorkManager; 069import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling; 070import org.nuxeo.runtime.api.Framework; 071import org.nuxeo.runtime.metrics.MetricsService; 072import org.nuxeo.runtime.transaction.TransactionHelper; 073 074import com.codahale.metrics.MetricRegistry; 075import com.codahale.metrics.SharedMetricRegistries; 076import com.codahale.metrics.Timer; 077 078/** 079 * The session is the main high level access point to data from the underlying database. 080 */ 081public class SessionImpl implements Session, XAResource { 082 083 private static final Log log = LogFactory.getLog(SessionImpl.class); 084 085 /** 086 * Set this system property to false if you don't want repositories to be looked up under the compatibility name 087 * "default" in the "repositories" table. 088 * <p> 089 * Only do this if you start from an empty database, or if you have migrated the "repositories" table by hand, or if 090 * you need to create a new repository in a database already containing a "default" repository (table sharing, not 091 * recommended). 092 */ 093 public static final String COMPAT_REPOSITORY_NAME_KEY = "org.nuxeo.vcs.repository.name.default.compat"; 094 095 private static final boolean COMPAT_REPOSITORY_NAME = Boolean.parseBoolean(Framework.getProperty( 096 COMPAT_REPOSITORY_NAME_KEY, "true")); 097 098 protected final RepositoryImpl repository; 099 100 private final Mapper mapper; 101 102 private final Model model; 103 104 protected final FulltextParser fulltextParser; 105 106 // public because used by unit tests 107 public final PersistenceContext context; 108 109 private volatile boolean live; 110 111 private boolean inTransaction; 112 113 private Node rootNode; 114 115 private long threadId; 116 117 private String threadName; 118 119 private Throwable threadStack; 120 121 private boolean readAclsChanged; 122 123 // @since 5.7 124 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 125 126 private final Timer saveTimer; 127 128 private final Timer queryTimer; 129 130 private final Timer aclrUpdateTimer; 131 132 private static final java.lang.String LOG_MIN_DURATION_KEY = "org.nuxeo.vcs.query.log_min_duration_ms"; 133 134 private static final long LOG_MIN_DURATION_NS = Long.parseLong(Framework.getProperty(LOG_MIN_DURATION_KEY, "-1")) * 1000000; 135 136 public SessionImpl(RepositoryImpl repository, Model model, Mapper mapper) { 137 this.repository = repository; 138 this.mapper = mapper; 139 this.model = model; 140 context = new PersistenceContext(model, mapper, this); 141 live = true; 142 readAclsChanged = false; 143 144 try { 145 fulltextParser = repository.fulltextParserClass.newInstance(); 146 } catch (ReflectiveOperationException e) { 147 throw new NuxeoException(e); 148 } 149 saveTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), "saves")); 150 queryTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), "queries")); 151 aclrUpdateTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), 152 "aclr-updates")); 153 154 computeRootNode(); 155 } 156 157 public void checkLive() { 158 if (!live) { 159 throw new IllegalStateException("Session is not live"); 160 } 161 checkThread(); 162 } 163 164 // called by NetServlet when forwarding remote NetMapper calls. 165 @Override 166 public Mapper getMapper() { 167 return mapper; 168 } 169 170 /** 171 * Gets the XAResource. Called by the ManagedConnectionImpl, which actually wraps it in a connection-aware 172 * implementation. 173 */ 174 public XAResource getXAResource() { 175 return this; 176 } 177 178 /** 179 * Clears all the caches. Called by RepositoryManagement. 180 */ 181 protected int clearCaches() { 182 if (inTransaction) { 183 // avoid potential multi-threaded access to active session 184 return 0; 185 } 186 checkThreadEnd(); 187 return context.clearCaches(); 188 } 189 190 protected PersistenceContext getContext() { 191 return context; 192 } 193 194 protected void rollback() { 195 context.clearCaches(); 196 } 197 198 protected void checkThread() { 199 if (threadId == 0) { 200 return; 201 } 202 long currentThreadId = Thread.currentThread().getId(); 203 if (threadId == currentThreadId) { 204 return; 205 } 206 String currentThreadName = Thread.currentThread().getName(); 207 String msg = String.format("Concurrency Error: Session was started in thread %s (%s)" 208 + " but is being used in thread %s (%s)", threadId, threadName, currentThreadId, currentThreadName); 209 throw new IllegalStateException(msg, threadStack); 210 } 211 212 protected void checkThreadStart() { 213 threadId = Thread.currentThread().getId(); 214 threadName = Thread.currentThread().getName(); 215 if (log.isDebugEnabled()) { 216 threadStack = new Throwable("owner stack trace"); 217 } 218 } 219 220 protected void checkThreadEnd() { 221 threadId = 0; 222 threadName = null; 223 threadStack = null; 224 } 225 226 /** 227 * Generates a new id, or used a pre-generated one (import). 228 */ 229 protected Serializable generateNewId(Serializable id) { 230 return context.generateNewId(id); 231 } 232 233 protected boolean isIdNew(Serializable id) { 234 return context.isIdNew(id); 235 } 236 237 /* 238 * ----- javax.resource.cci.Connection ----- 239 */ 240 241 @Override 242 public void close() throws ResourceException { 243 try { 244 checkLive(); 245 closeSession(); 246 repository.closeSession(this); 247 } catch (Exception cause) { 248 throw new ResourceException(cause); 249 } 250 } 251 252 protected void closeSession() { 253 live = false; 254 context.clearCaches(); 255 // close the mapper and therefore the connection 256 mapper.close(); 257 // don't clean the caches, we keep the pristine cache around 258 // TODO this is getting destroyed, we can clean everything 259 } 260 261 @Override 262 public Interaction createInteraction() throws ResourceException { 263 throw new UnsupportedOperationException(); 264 } 265 266 @Override 267 public LocalTransaction getLocalTransaction() throws ResourceException { 268 throw new UnsupportedOperationException(); 269 } 270 271 @Override 272 public ConnectionMetaData getMetaData() throws ResourceException { 273 throw new UnsupportedOperationException(); 274 } 275 276 @Override 277 public ResultSetInfo getResultSetInfo() throws ResourceException { 278 throw new UnsupportedOperationException(); 279 } 280 281 /* 282 * ----- Session ----- 283 */ 284 285 @Override 286 public boolean isLive() { 287 return live; 288 } 289 290 @Override 291 public boolean isStateSharedByAllThreadSessions() { 292 // only the JCA handle returns true 293 return false; 294 } 295 296 @Override 297 public String getRepositoryName() { 298 return repository.getName(); 299 } 300 301 @Override 302 public Model getModel() { 303 return model; 304 } 305 306 @Override 307 public Node getRootNode() { 308 checkLive(); 309 return rootNode; 310 } 311 312 @Override 313 public void save() { 314 final Timer.Context timerContext = saveTimer.time(); 315 try { 316 checkLive(); 317 flush(); 318 if (!inTransaction) { 319 sendInvalidationsToOthers(); 320 // as we don't have a way to know when the next 321 // non-transactional 322 // statement will start, process invalidations immediately 323 } 324 processReceivedInvalidations(); 325 } finally { 326 timerContext.stop(); 327 } 328 } 329 330 protected void flush() { 331 checkThread(); 332 List<Work> works; 333 if (!repository.getRepositoryDescriptor().getFulltextDescriptor().getFulltextDisabled()) { 334 works = getFulltextWorks(); 335 } else { 336 works = Collections.emptyList(); 337 } 338 doFlush(); 339 if (readAclsChanged) { 340 updateReadAcls(); 341 } 342 scheduleWork(works); 343 checkInvalidationsConflict(); 344 } 345 346 protected void scheduleWork(List<Work> works) { 347 // do async fulltext indexing only if high-level sessions are available 348 RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class); 349 if (repositoryManager != null && !works.isEmpty()) { 350 WorkManager workManager = Framework.getLocalService(WorkManager.class); 351 for (Work work : works) { 352 // schedule work post-commit 353 // in non-tx mode, this may execute it nearly immediately 354 workManager.schedule(work, Scheduling.IF_NOT_SCHEDULED, true); 355 } 356 } 357 } 358 359 protected void doFlush() { 360 List<Fragment> fragmentsToClearDirty = new ArrayList<>(0); 361 RowBatch batch = context.getSaveBatch(fragmentsToClearDirty); 362 if (!batch.isEmpty()) { 363 log.debug("Saving session"); 364 // execute the batch 365 mapper.write(batch); 366 log.debug("End of save"); 367 for (Fragment fragment : fragmentsToClearDirty) { 368 fragment.clearDirty(); 369 } 370 } 371 } 372 373 protected Serializable getContainingDocument(Serializable id) { 374 return context.getContainingDocument(id); 375 } 376 377 /** 378 * Gets the fulltext updates to do. Called at save() time. 379 * 380 * @return a list of {@link Work} instances to schedule post-commit. 381 */ 382 protected List<Work> getFulltextWorks() { 383 Set<Serializable> dirtyStrings = new HashSet<Serializable>(); 384 Set<Serializable> dirtyBinaries = new HashSet<Serializable>(); 385 context.findDirtyDocuments(dirtyStrings, dirtyBinaries); 386 if (dirtyStrings.isEmpty() && dirtyBinaries.isEmpty()) { 387 return Collections.emptyList(); 388 } 389 390 List<Work> works = new LinkedList<Work>(); 391 getFulltextSimpleWorks(works, dirtyStrings); 392 getFulltextBinariesWorks(works, dirtyBinaries); 393 return works; 394 } 395 396 protected void getFulltextSimpleWorks(List<Work> works, Set<Serializable> dirtyStrings) { 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 (!model.getFulltextConfiguration().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 : model.getFulltextConfiguration().indexNames) { 424 Set<String> paths; 425 if (model.getFulltextConfiguration().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 = model.getFulltextConfiguration().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 /** 885 * Gets complex children names defined by the primary type and the list of mixins. 886 */ 887 protected Set<String> getChildrenNames(String primaryType, List<String> mixins) { 888 Map<String, String> cc = model.getTypeComplexChildren(primaryType); 889 if (cc == null) { 890 cc = Collections.emptyMap(); 891 } 892 Set<String> childrenNames = new HashSet<>(cc.keySet()); 893 for (String mixin : mixins) { 894 cc = model.getMixinComplexChildren(mixin); 895 if (cc != null) { 896 childrenNames.addAll(cc.keySet()); 897 } 898 } 899 return childrenNames; 900 } 901 902 @Override 903 public Node addChildNode(Node parent, String name, Long pos, String typeName, boolean complexProp) { 904 if (pos == null && !complexProp && parent != null) { 905 pos = context.getNextPos(parent.getId(), complexProp); 906 } 907 return addChildNode(null, parent, name, pos, typeName, complexProp); 908 } 909 910 @Override 911 public Node addChildNode(Serializable id, Node parent, String name, Long pos, String typeName, 912 boolean complexProp) { 913 checkLive(); 914 if (name == null) { 915 throw new IllegalArgumentException("Illegal null name"); 916 } 917 name = normalize(name); 918 if (name.contains("/") || name.equals(".") || name.equals("..")) { 919 throw new IllegalArgumentException("Illegal name: " + name); 920 } 921 if (!model.isType(typeName)) { 922 throw new IllegalArgumentException("Unknown type: " + typeName); 923 } 924 id = generateNewId(id); 925 Serializable parentId = parent == null ? null : parent.hierFragment.getId(); 926 Node node = addNode(id, parentId, name, pos, typeName, complexProp); 927 // immediately create child nodes (for complex properties) in order 928 // to avoid concurrency issue later on 929 Map<String, String> childrenTypes = model.getTypeComplexChildren(typeName); 930 for (Entry<String, String> es : childrenTypes.entrySet()) { 931 String childName = es.getKey(); 932 String childType = es.getValue(); 933 addChildNode(node, childName, null, childType, true); 934 } 935 return node; 936 } 937 938 protected Node addNode(Serializable id, Serializable parentId, String name, Long pos, String typeName, 939 boolean complexProp) { 940 requireReadAclsUpdate(); 941 // main info 942 Row hierRow = new Row(Model.HIER_TABLE_NAME, id); 943 hierRow.putNew(Model.HIER_PARENT_KEY, parentId); 944 hierRow.putNew(Model.HIER_CHILD_NAME_KEY, name); 945 hierRow.putNew(Model.HIER_CHILD_POS_KEY, pos); 946 hierRow.putNew(Model.MAIN_PRIMARY_TYPE_KEY, typeName); 947 hierRow.putNew(Model.HIER_CHILD_ISPROPERTY_KEY, Boolean.valueOf(complexProp)); 948 SimpleFragment hierFragment = context.createHierarchyFragment(hierRow); 949 FragmentGroup fragmentGroup = new FragmentGroup(hierFragment, new FragmentsMap()); 950 return new Node(context, fragmentGroup, context.getPath(hierFragment)); 951 } 952 953 @Override 954 public Node addProxy(Serializable targetId, Serializable versionableId, Node parent, String name, Long pos) { 955 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 956 throw new NuxeoException("Proxies are disabled by configuration"); 957 } 958 Node proxy = addChildNode(parent, name, pos, Model.PROXY_TYPE, false); 959 proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId); 960 proxy.setSimpleProperty(Model.PROXY_VERSIONABLE_PROP, versionableId); 961 SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME); 962 context.createdProxyFragment(proxyFragment); 963 return proxy; 964 } 965 966 @Override 967 public void setProxyTarget(Node proxy, Serializable targetId) { 968 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 969 throw new NuxeoException("Proxies are disabled by configuration"); 970 } 971 SimpleProperty prop = proxy.getSimpleProperty(Model.PROXY_TARGET_PROP); 972 Serializable oldTargetId = prop.getValue(); 973 if (!oldTargetId.equals(targetId)) { 974 SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME); 975 context.removedProxyTarget(proxyFragment); 976 proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId); 977 context.addedProxyTarget(proxyFragment); 978 } 979 } 980 981 @Override 982 public boolean hasChildNode(Node parent, String name, boolean complexProp) { 983 checkLive(); 984 // TODO could optimize further by not fetching the fragment at all 985 SimpleFragment fragment = context.getChildHierByName(parent.getId(), normalize(name), complexProp); 986 return fragment != null; 987 } 988 989 @Override 990 public Node getChildNode(Node parent, String name, boolean complexProp) { 991 checkLive(); 992 if (name == null || name.contains("/") || name.equals(".") || name.equals("..")) { 993 throw new IllegalArgumentException("Illegal name: " + name); 994 } 995 SimpleFragment fragment = context.getChildHierByName(parent.getId(), name, complexProp); 996 return fragment == null ? null : getNodeById(fragment.getId()); 997 } 998 999 // TODO optimize with dedicated backend call 1000 @Override 1001 public boolean hasChildren(Node parent, boolean complexProp) { 1002 checkLive(); 1003 List<SimpleFragment> children = context.getChildren(parent.getId(), null, complexProp); 1004 if (complexProp) { 1005 return !children.isEmpty(); 1006 } 1007 if (children.isEmpty()) { 1008 return false; 1009 } 1010 // we have to check that type names are not obsolete, as they wouldn't be returned 1011 // by getChildren and we must be consistent 1012 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 1013 for (SimpleFragment simpleFragment : children) { 1014 String primaryType = simpleFragment.getString(Model.MAIN_PRIMARY_TYPE_KEY); 1015 if (primaryType.equals(Model.PROXY_TYPE)) { 1016 Node node = getNodeById(simpleFragment.getId(), false); 1017 Serializable targetId = node.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue(); 1018 if (targetId == null) { 1019 // missing target, should not happen, ignore 1020 continue; 1021 } 1022 Node target = getNodeById(targetId, false); 1023 if (target == null) { 1024 continue; 1025 } 1026 primaryType = target.getPrimaryType(); 1027 } 1028 DocumentType type = schemaManager.getDocumentType(primaryType); 1029 if (type == null) { 1030 // obsolete type, ignored in getChildren 1031 continue; 1032 } 1033 return true; 1034 } 1035 return false; 1036 } 1037 1038 @Override 1039 public List<Node> getChildren(Node parent, String name, boolean complexProp) { 1040 checkLive(); 1041 List<SimpleFragment> fragments = context.getChildren(parent.getId(), name, complexProp); 1042 List<Node> nodes = new ArrayList<Node>(fragments.size()); 1043 for (SimpleFragment fragment : fragments) { 1044 Node node = getNodeById(fragment.getId()); 1045 if (node == null) { 1046 // cannot happen 1047 log.error("Child node cannot be created: " + fragment.getId()); 1048 continue; 1049 } 1050 nodes.add(node); 1051 } 1052 return nodes; 1053 } 1054 1055 @Override 1056 public void orderBefore(Node parent, Node source, Node dest) { 1057 checkLive(); 1058 context.orderBefore(parent.getId(), source.getId(), dest == null ? null : dest.getId()); 1059 } 1060 1061 @Override 1062 public Node move(Node source, Node parent, String name) { 1063 checkLive(); 1064 if (!parent.getId().equals(source.getParentId())) { 1065 flush(); // needed when doing many moves for circular stuff 1066 } 1067 context.move(source, parent.getId(), name); 1068 requireReadAclsUpdate(); 1069 return source; 1070 } 1071 1072 @Override 1073 public Node copy(Node source, Node parent, String name) { 1074 checkLive(); 1075 flush(); 1076 Serializable id = context.copy(source, parent.getId(), name); 1077 requireReadAclsUpdate(); 1078 return getNodeById(id); 1079 } 1080 1081 @Override 1082 public void removeNode(Node node) { 1083 checkLive(); 1084 flush(); 1085 // remove the lock using the lock manager 1086 // TODO children locks? 1087 getLockManager().removeLock(model.idToString(node.getId()), null); 1088 context.removeNode(node.getHierFragment()); 1089 } 1090 1091 @Override 1092 public void removePropertyNode(Node node) { 1093 checkLive(); 1094 // no flush needed 1095 context.removePropertyNode(node.getHierFragment()); 1096 } 1097 1098 @Override 1099 public Node checkIn(Node node, String label, String checkinComment) { 1100 checkLive(); 1101 flush(); 1102 Serializable id = context.checkIn(node, label, checkinComment); 1103 requireReadAclsUpdate(); 1104 // save to reflect changes immediately in database 1105 flush(); 1106 return getNodeById(id); 1107 } 1108 1109 @Override 1110 public void checkOut(Node node) { 1111 checkLive(); 1112 context.checkOut(node); 1113 requireReadAclsUpdate(); 1114 } 1115 1116 @Override 1117 public void restore(Node node, Node version) { 1118 checkLive(); 1119 // save done inside method 1120 context.restoreVersion(node, version); 1121 requireReadAclsUpdate(); 1122 } 1123 1124 @Override 1125 public Node getVersionByLabel(Serializable versionSeriesId, String label) { 1126 if (label == null) { 1127 return null; 1128 } 1129 List<Node> versions = getVersions(versionSeriesId); 1130 for (Node node : versions) { 1131 String l = (String) node.getSimpleProperty(Model.VERSION_LABEL_PROP).getValue(); 1132 if (label.equals(l)) { 1133 return node; 1134 } 1135 } 1136 return null; 1137 } 1138 1139 @Override 1140 public Node getLastVersion(Serializable versionSeriesId) { 1141 checkLive(); 1142 List<Serializable> ids = context.getVersionIds(versionSeriesId); 1143 return ids.isEmpty() ? null : getNodeById(ids.get(ids.size() - 1)); 1144 } 1145 1146 @Override 1147 public List<Node> getVersions(Serializable versionSeriesId) { 1148 checkLive(); 1149 List<Serializable> ids = context.getVersionIds(versionSeriesId); 1150 List<Node> nodes = new ArrayList<Node>(ids.size()); 1151 for (Serializable id : ids) { 1152 nodes.add(getNodeById(id)); 1153 } 1154 return nodes; 1155 } 1156 1157 @Override 1158 public List<Node> getProxies(Node document, Node parent) { 1159 checkLive(); 1160 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 1161 return Collections.emptyList(); 1162 } 1163 1164 List<Serializable> ids; 1165 if (document.isVersion()) { 1166 ids = context.getTargetProxyIds(document.getId()); 1167 } else { 1168 Serializable versionSeriesId; 1169 if (document.isProxy()) { 1170 versionSeriesId = document.getSimpleProperty(Model.PROXY_VERSIONABLE_PROP).getValue(); 1171 } else { 1172 versionSeriesId = document.getId(); 1173 } 1174 ids = context.getSeriesProxyIds(versionSeriesId); 1175 } 1176 1177 List<Node> nodes = new LinkedList<Node>(); 1178 for (Serializable id : ids) { 1179 Node node = getNodeById(id); 1180 if (node != null || Boolean.TRUE.booleanValue()) { // XXX 1181 // null if deleted, which means selection wasn't correctly 1182 // updated 1183 nodes.add(node); 1184 } 1185 } 1186 1187 if (parent != null) { 1188 // filter by parent 1189 Serializable parentId = parent.getId(); 1190 for (Iterator<Node> it = nodes.iterator(); it.hasNext();) { 1191 Node node = it.next(); 1192 if (!parentId.equals(node.getParentId())) { 1193 it.remove(); 1194 } 1195 } 1196 } 1197 1198 return nodes; 1199 } 1200 1201 /** 1202 * Fetches the hierarchy fragment for the given rows and all their ancestors. 1203 * 1204 * @param ids the fragment ids 1205 */ 1206 protected List<Fragment> getHierarchyAndAncestors(Collection<Serializable> ids) { 1207 Set<Serializable> allIds = mapper.getAncestorsIds(ids); 1208 allIds.addAll(ids); 1209 List<RowId> rowIds = new ArrayList<RowId>(allIds.size()); 1210 for (Serializable id : allIds) { 1211 rowIds.add(new RowId(Model.HIER_TABLE_NAME, id)); 1212 } 1213 return context.getMulti(rowIds, true); 1214 } 1215 1216 @Override 1217 public PartialList<Serializable> query(String query, QueryFilter queryFilter, boolean countTotal) { 1218 final Timer.Context timerContext = queryTimer.time(); 1219 try { 1220 return mapper.query(query, NXQL.NXQL, queryFilter, countTotal); 1221 } finally { 1222 timerContext.stop(); 1223 } 1224 } 1225 1226 @Override 1227 public PartialList<Serializable> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) { 1228 final Timer.Context timerContext = queryTimer.time(); 1229 try { 1230 return mapper.query(query, queryType, queryFilter, countUpTo); 1231 } finally { 1232 long duration = timerContext.stop(); 1233 if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) { 1234 String msg = String.format("duration_ms:\t%.2f\t%s %s\tquery\t%s", duration / 1000000.0, queryFilter, 1235 countUpToAsString(countUpTo), query); 1236 if (log.isTraceEnabled()) { 1237 log.info(msg, new Throwable("Slow query stack trace")); 1238 } else { 1239 log.info(msg); 1240 } 1241 } 1242 } 1243 } 1244 1245 private String countUpToAsString(long countUpTo) { 1246 if (countUpTo > 0) { 1247 return String.format("count total results up to %d", countUpTo); 1248 } 1249 return countUpTo == -1 ? "count total results UNLIMITED" : ""; 1250 } 1251 1252 @Override 1253 public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, 1254 Object... params) { 1255 return queryAndFetch(query, queryType, queryFilter, false, params); 1256 } 1257 1258 @Override 1259 public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, 1260 boolean distinctDocuments, Object... params) { 1261 final Timer.Context timerContext = queryTimer.time(); 1262 try { 1263 return mapper.queryAndFetch(query, queryType, queryFilter, distinctDocuments, params); 1264 } finally { 1265 long duration = timerContext.stop(); 1266 if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) { 1267 String msg = String.format("duration_ms:\t%.2f\t%s\tqueryAndFetch\t%s", duration / 1000000.0, 1268 queryFilter, query); 1269 if (log.isTraceEnabled()) { 1270 log.info(msg, new Throwable("Slow query stack trace")); 1271 } else { 1272 log.info(msg); 1273 } 1274 } 1275 } 1276 } 1277 1278 @Override 1279 public LockManager getLockManager() { 1280 return repository.getLockManager(); 1281 } 1282 1283 @Override 1284 public void requireReadAclsUpdate() { 1285 readAclsChanged = true; 1286 } 1287 1288 @Override 1289 public void updateReadAcls() { 1290 final Timer.Context timerContext = aclrUpdateTimer.time(); 1291 try { 1292 mapper.updateReadAcls(); 1293 readAclsChanged = false; 1294 } finally { 1295 timerContext.stop(); 1296 } 1297 } 1298 1299 @Override 1300 public void rebuildReadAcls() { 1301 mapper.rebuildReadAcls(); 1302 readAclsChanged = false; 1303 } 1304 1305 private void computeRootNode() { 1306 String repositoryId = repository.getName(); 1307 Serializable rootId = mapper.getRootId(repositoryId); 1308 if (rootId == null && COMPAT_REPOSITORY_NAME) { 1309 // compat, old repositories had fixed id "default" 1310 rootId = mapper.getRootId("default"); 1311 } 1312 if (rootId == null) { 1313 log.debug("Creating root"); 1314 rootNode = addRootNode(); 1315 addRootACP(); 1316 save(); 1317 // record information about the root id 1318 mapper.setRootId(repositoryId, rootNode.getId()); 1319 } else { 1320 rootNode = getNodeById(rootId, false); 1321 } 1322 } 1323 1324 // TODO factor with addChildNode 1325 private Node addRootNode() { 1326 Serializable id = generateNewId(null); 1327 return addNode(id, null, "", null, Model.ROOT_TYPE, false); 1328 } 1329 1330 private void addRootACP() { 1331 ACLRow[] aclrows = new ACLRow[3]; 1332 // TODO put groups in their proper place. like that now for consistency. 1333 aclrows[0] = new ACLRow(0, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATORS, 1334 null); 1335 aclrows[1] = new ACLRow(1, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATOR, 1336 null); 1337 aclrows[2] = new ACLRow(2, ACL.LOCAL_ACL, true, SecurityConstants.READ, SecurityConstants.MEMBERS, null); 1338 rootNode.setCollectionProperty(Model.ACL_PROP, aclrows); 1339 requireReadAclsUpdate(); 1340 } 1341 1342 // public Node newNodeInstance() needed ? 1343 1344 public void checkPermission(String absPath, String actions) { 1345 checkLive(); 1346 // TODO Auto-generated method stub 1347 throw new RuntimeException("Not implemented"); 1348 } 1349 1350 public boolean hasPendingChanges() { 1351 checkLive(); 1352 // TODO Auto-generated method stub 1353 throw new RuntimeException("Not implemented"); 1354 } 1355 1356 public void markReferencedBinaries() { 1357 checkLive(); 1358 mapper.markReferencedBinaries(); 1359 } 1360 1361 public int cleanupDeletedDocuments(int max, Calendar beforeTime) { 1362 checkLive(); 1363 if (!repository.getRepositoryDescriptor().getSoftDeleteEnabled()) { 1364 return 0; 1365 } 1366 return mapper.cleanupDeletedRows(max, beforeTime); 1367 } 1368 1369 /* 1370 * ----- XAResource ----- 1371 */ 1372 1373 @Override 1374 public boolean isSameRM(XAResource xaresource) { 1375 return xaresource == this; 1376 } 1377 1378 @Override 1379 public void start(Xid xid, int flags) throws XAException { 1380 if (flags == TMNOFLAGS) { 1381 try { 1382 processReceivedInvalidations(); 1383 } catch (NuxeoException e) { 1384 log.error("Could not start transaction", e); 1385 throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e); 1386 } 1387 } 1388 mapper.start(xid, flags); 1389 inTransaction = true; 1390 checkThreadStart(); 1391 } 1392 1393 @Override 1394 public void end(Xid xid, int flags) throws XAException { 1395 boolean failed = true; 1396 try { 1397 if (flags != TMFAIL) { 1398 try { 1399 flush(); 1400 } catch (ConcurrentUpdateException e) { 1401 TransactionHelper.noteSuppressedException(e); 1402 log.debug("Exception during transaction commit", e); 1403 // set rollback only manually instead of throwing, this avoids 1404 // a spurious log in Geronimo TransactionImpl and has the same effect 1405 TransactionHelper.setTransactionRollbackOnly(); 1406 return; 1407 } catch (NuxeoException e) { 1408 log.error("Exception during transaction commit", e); 1409 throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e); 1410 } 1411 } 1412 failed = false; 1413 mapper.end(xid, flags); 1414 } finally { 1415 if (failed) { 1416 mapper.end(xid, TMFAIL); 1417 // rollback done by tx manager 1418 } 1419 } 1420 } 1421 1422 @Override 1423 public int prepare(Xid xid) throws XAException { 1424 int res = mapper.prepare(xid); 1425 if (res == XA_RDONLY) { 1426 // Read-only optimization, commit() won't be called by the TM. 1427 // It's important to nevertheless send invalidations because 1428 // Oracle, in tightly-coupled transaction mode, can return 1429 // this status even when some changes were actually made 1430 // (they just will be committed by another resource). 1431 // See NXP-7943 1432 commitDone(); 1433 } 1434 return res; 1435 } 1436 1437 @Override 1438 public void commit(Xid xid, boolean onePhase) throws XAException { 1439 try { 1440 mapper.commit(xid, onePhase); 1441 } finally { 1442 commitDone(); 1443 } 1444 } 1445 1446 protected void commitDone() throws XAException { 1447 inTransaction = false; 1448 try { 1449 try { 1450 sendInvalidationsToOthers(); 1451 } finally { 1452 checkThreadEnd(); 1453 } 1454 } catch (NuxeoException e) { 1455 log.error("Could not send invalidations", e); 1456 throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e); 1457 } 1458 } 1459 1460 @Override 1461 public void rollback(Xid xid) throws XAException { 1462 try { 1463 try { 1464 mapper.rollback(xid); 1465 } finally { 1466 rollback(); 1467 } 1468 } finally { 1469 inTransaction = false; 1470 // no invalidations to send 1471 checkThreadEnd(); 1472 } 1473 } 1474 1475 @Override 1476 public void forget(Xid xid) throws XAException { 1477 mapper.forget(xid); 1478 } 1479 1480 @Override 1481 public Xid[] recover(int flag) throws XAException { 1482 return mapper.recover(flag); 1483 } 1484 1485 @Override 1486 public boolean setTransactionTimeout(int seconds) throws XAException { 1487 return mapper.setTransactionTimeout(seconds); 1488 } 1489 1490 @Override 1491 public int getTransactionTimeout() throws XAException { 1492 return mapper.getTransactionTimeout(); 1493 } 1494 1495 public long getCacheSize() { 1496 return context.getCacheSize(); 1497 } 1498 1499 public long getCacheMapperSize() { 1500 return context.getCacheMapperSize(); 1501 } 1502 1503 public long getCachePristineSize() { 1504 return context.getCachePristineSize(); 1505 } 1506 1507 public long getCacheSelectionSize() { 1508 return context.getCacheSelectionSize(); 1509 } 1510 1511 @Override 1512 public Map<String, String> getBinaryFulltext(Serializable id) { 1513 if (repository.getRepositoryDescriptor().getFulltextDescriptor().getFulltextDisabled()) { 1514 return null; 1515 } 1516 RowId rowId = new RowId(Model.FULLTEXT_TABLE_NAME, id); 1517 return mapper.getBinaryFulltext(rowId); 1518 } 1519 1520}