001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 */ 012package org.nuxeo.ecm.core.storage.sql; 013 014import java.io.Serializable; 015import java.util.ArrayList; 016import java.util.Arrays; 017import java.util.Collection; 018import java.util.Collections; 019import java.util.Deque; 020import java.util.GregorianCalendar; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashSet; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Map; 027import java.util.Map.Entry; 028import java.util.Set; 029 030import org.apache.commons.collections.map.AbstractReferenceMap; 031import org.apache.commons.collections.map.ReferenceMap; 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034import org.nuxeo.common.utils.StringUtils; 035import org.nuxeo.ecm.core.api.DocumentExistsException; 036import org.nuxeo.ecm.core.api.NuxeoException; 037import org.nuxeo.ecm.core.schema.FacetNames; 038import org.nuxeo.ecm.core.storage.sql.Fragment.State; 039import org.nuxeo.ecm.core.storage.sql.RowMapper.CopyResult; 040import org.nuxeo.ecm.core.storage.sql.RowMapper.IdWithTypes; 041import org.nuxeo.ecm.core.storage.sql.RowMapper.NodeInfo; 042import org.nuxeo.ecm.core.storage.sql.RowMapper.RowBatch; 043import org.nuxeo.ecm.core.storage.sql.RowMapper.RowUpdate; 044import org.nuxeo.ecm.core.storage.sql.SimpleFragment.FieldComparator; 045import org.nuxeo.runtime.api.Framework; 046import org.nuxeo.runtime.metrics.MetricsService; 047 048import com.codahale.metrics.Counter; 049import com.codahale.metrics.MetricRegistry; 050import com.codahale.metrics.SharedMetricRegistries; 051 052/** 053 * This class holds persistence context information. 054 * <p> 055 * All non-saved modified data is referenced here. At save time, the data is sent to the database by the {@link Mapper}. 056 * The database will at some time later be committed by the external transaction manager in effect. 057 * <p> 058 * Internally a fragment can be in at most one of the "pristine" or "modified" map. After a save() all the fragments are 059 * pristine, and may be partially invalidated after commit by other local or clustered contexts that committed too. 060 * <p> 061 * Depending on the table, the context may hold {@link SimpleFragment}s, which represent one row, 062 * {@link CollectionFragment}s, which represent several rows. 063 * <p> 064 * This class is not thread-safe, it should be tied to a single session and the session itself should not be used 065 * concurrently. 066 */ 067public class PersistenceContext { 068 069 protected static final Log log = LogFactory.getLog(PersistenceContext.class); 070 071 /** 072 * Property for threshold at which we warn that a Selection may be too big, with stack trace. 073 * 074 * @since 7.1 075 */ 076 public static final String SEL_WARN_THRESHOLD_PROP = "org.nuxeo.vcs.selection.warn.threshold"; 077 078 public static final String SEL_WARN_THRESHOLD_DEFAULT = "15000"; 079 080 protected static final FieldComparator POS_COMPARATOR = new FieldComparator(Model.HIER_CHILD_POS_KEY); 081 082 protected static final FieldComparator VER_CREATED_COMPARATOR = new FieldComparator(Model.VERSION_CREATED_KEY); 083 084 protected final Model model; 085 086 // protected because accessed by Fragment.refetch() 087 protected final RowMapper mapper; 088 089 protected final SessionImpl session; 090 091 // selection context for complex properties 092 protected final SelectionContext hierComplex; 093 094 // selection context for non-complex properties 095 // public because used by unit tests 096 public final SelectionContext hierNonComplex; 097 098 // selection context for versions by series 099 private final SelectionContext seriesVersions; 100 101 // selection context for proxies by series 102 private final SelectionContext seriesProxies; 103 104 // selection context for proxies by target 105 private final SelectionContext targetProxies; 106 107 private final List<SelectionContext> selections; 108 109 /** 110 * The pristine fragments. All held data is identical to what is present in the database and could be refetched if 111 * needed. 112 * <p> 113 * This contains fragment that are {@link State#PRISTINE} or {@link State#ABSENT}, or in some cases 114 * {@link State#INVALIDATED_MODIFIED} or {@link State#INVALIDATED_DELETED}. 115 * <p> 116 * Pristine fragments must be kept here when referenced by the application, because the application must get the 117 * same fragment object if asking for it twice, even in two successive transactions. 118 * <p> 119 * This is memory-sensitive, a fragment can always be refetched if nobody uses it and the GC collects it. Use a weak 120 * reference for the values, we don't hold them longer than they need to be referenced, as the underlying mapper 121 * also has its own cache. 122 */ 123 protected final Map<RowId, Fragment> pristine; 124 125 /** 126 * The fragments changed by the session. 127 * <p> 128 * This contains fragment that are {@link State#CREATED}, {@link State#MODIFIED} or {@link State#DELETED}. 129 */ 130 protected final Map<RowId, Fragment> modified; 131 132 /** 133 * Fragment ids generated but not yet saved. We know that any fragment with one of these ids cannot exist in the 134 * database. 135 */ 136 private final Set<Serializable> createdIds; 137 138 /** 139 * Cache statistics 140 * 141 * @since 5.7 142 */ 143 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 144 145 protected final Counter cacheCount; 146 147 protected final Counter cacheHitCount; 148 149 /** 150 * Threshold at which we warn that a Selection may be too big, with stack trace. 151 */ 152 protected long bigSelWarnThreshold; 153 154 @SuppressWarnings("unchecked") 155 public PersistenceContext(Model model, RowMapper mapper, SessionImpl session) { 156 this.model = model; 157 this.mapper = mapper; 158 this.session = session; 159 hierComplex = new SelectionContext(SelectionType.CHILDREN, Boolean.TRUE, mapper, this); 160 hierNonComplex = new SelectionContext(SelectionType.CHILDREN, Boolean.FALSE, mapper, this); 161 seriesVersions = new SelectionContext(SelectionType.SERIES_VERSIONS, null, mapper, this); 162 selections = new ArrayList<SelectionContext>(Arrays.asList(hierComplex, hierNonComplex, seriesVersions)); 163 if (model.proxiesEnabled) { 164 seriesProxies = new SelectionContext(SelectionType.SERIES_PROXIES, null, mapper, this); 165 targetProxies = new SelectionContext(SelectionType.TARGET_PROXIES, null, mapper, this); 166 selections.add(seriesProxies); 167 selections.add(targetProxies); 168 } else { 169 seriesProxies = null; 170 targetProxies = null; 171 } 172 173 // use a weak reference for the values, we don't hold them longer than 174 // they need to be referenced, as the underlying mapper also has its own 175 // cache 176 pristine = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK); 177 modified = new HashMap<RowId, Fragment>(); 178 // this has to be linked to keep creation order, as foreign keys 179 // are used and need this 180 createdIds = new LinkedHashSet<Serializable>(); 181 cacheCount = registry.counter(MetricRegistry.name("nuxeo", "repositories", session.getRepositoryName(), 182 "caches", "count")); 183 cacheHitCount = registry.counter(MetricRegistry.name("nuxeo", "repositories", session.getRepositoryName(), 184 "caches", "hit")); 185 try { 186 bigSelWarnThreshold = Long.parseLong(Framework.getProperty(SEL_WARN_THRESHOLD_PROP, SEL_WARN_THRESHOLD_DEFAULT)); 187 } catch (NumberFormatException e) { 188 log.error("Invalid value for " + SEL_WARN_THRESHOLD_PROP + ": " + Framework.getProperty(SEL_WARN_THRESHOLD_PROP)); 189 } 190 } 191 192 protected int clearCaches() { 193 mapper.clearCache(); 194 // TODO there should be a synchronization here 195 // but this is a rare operation and we don't call 196 // it if a transaction is in progress 197 int n = clearLocalCaches(); 198 modified.clear(); // not empty when rolling back before save 199 createdIds.clear(); 200 return n; 201 } 202 203 protected int clearLocalCaches() { 204 for (SelectionContext sel : selections) { 205 sel.clearCaches(); 206 } 207 int n = pristine.size(); 208 pristine.clear(); 209 return n; 210 } 211 212 protected long getCacheSize() { 213 return getCachePristineSize() + getCacheSelectionSize() + getCacheMapperSize(); 214 } 215 216 protected long getCacheMapperSize() { 217 return mapper.getCacheSize(); 218 } 219 220 protected long getCachePristineSize() { 221 return pristine.size(); 222 } 223 224 protected long getCacheSelectionSize() { 225 int size = 0; 226 for (SelectionContext sel : selections) { 227 size += sel.getSize(); 228 } 229 return size; 230 } 231 232 /** 233 * Generates a new id, or used a pre-generated one (import). 234 */ 235 protected Serializable generateNewId(Serializable id) { 236 if (id == null) { 237 id = mapper.generateNewId(); 238 } 239 createdIds.add(id); 240 return id; 241 } 242 243 protected boolean isIdNew(Serializable id) { 244 return createdIds.contains(id); 245 } 246 247 /** 248 * Saves all the created, modified and deleted rows into a batch object, for later execution. 249 * <p> 250 * Also updates the passed fragmentsToClearDirty list with dirty modified fragments, for later call of clearDirty 251 * (it's important to call it later and not now because for delta values we need the delta during batch write, and 252 * they are cleared by clearDirty). 253 */ 254 protected RowBatch getSaveBatch(List<Fragment> fragmentsToClearDirty) { 255 RowBatch batch = new RowBatch(); 256 257 // created main rows are saved first in the batch (in their order of 258 // creation), because they are used as foreign keys in all other tables 259 for (Serializable id : createdIds) { 260 RowId rowId = new RowId(Model.HIER_TABLE_NAME, id); 261 Fragment fragment = modified.remove(rowId); 262 if (fragment == null) { 263 // was created and deleted before save 264 continue; 265 } 266 batch.creates.add(fragment.row); 267 fragment.clearDirty(); 268 fragment.setPristine(); 269 pristine.put(rowId, fragment); 270 } 271 createdIds.clear(); 272 273 // save the rest 274 for (Entry<RowId, Fragment> en : modified.entrySet()) { 275 RowId rowId = en.getKey(); 276 Fragment fragment = en.getValue(); 277 switch (fragment.getState()) { 278 case CREATED: 279 batch.creates.add(fragment.row); 280 fragment.clearDirty(); 281 fragment.setPristine(); 282 // modified map cleared at end of loop 283 pristine.put(rowId, fragment); 284 break; 285 case MODIFIED: 286 if (fragment.row.isCollection()) { 287 if (((CollectionFragment) fragment).isDirty()) { 288 batch.updates.add(new RowUpdate(fragment.row, null)); 289 fragmentsToClearDirty.add(fragment); 290 } 291 } else { 292 Collection<String> keys = ((SimpleFragment) fragment).getDirtyKeys(); 293 if (!keys.isEmpty()) { 294 batch.updates.add(new RowUpdate(fragment.row, keys)); 295 fragmentsToClearDirty.add(fragment); 296 } 297 } 298 fragment.setPristine(); 299 // modified map cleared at end of loop 300 pristine.put(rowId, fragment); 301 break; 302 case DELETED: 303 // TODO deleting non-hierarchy fragments is done by the database 304 // itself as their foreign key to hierarchy is ON DELETE CASCADE 305 batch.deletes.add(new RowId(rowId)); 306 fragment.setDetached(); 307 // modified map cleared at end of loop 308 break; 309 case DELETED_DEPENDENT: 310 batch.deletesDependent.add(new RowId(rowId)); 311 fragment.setDetached(); 312 break; 313 case PRISTINE: 314 // cannot happen, but has been observed :( 315 log.error("Found PRISTINE fragment in modified map: " + fragment); 316 break; 317 default: 318 throw new RuntimeException(fragment.toString()); 319 } 320 } 321 modified.clear(); 322 323 // flush selections caches 324 for (SelectionContext sel : selections) { 325 sel.postSave(); 326 } 327 328 return batch; 329 } 330 331 private boolean complexProp(SimpleFragment fragment) { 332 return complexProp((Boolean) fragment.get(Model.HIER_CHILD_ISPROPERTY_KEY)); 333 } 334 335 private boolean complexProp(Boolean isProperty) { 336 return Boolean.TRUE.equals(isProperty); 337 } 338 339 private SelectionContext getHierSelectionContext(boolean complexProp) { 340 return complexProp ? hierComplex : hierNonComplex; 341 } 342 343 /** 344 * Finds the documents having dirty text or dirty binaries that have to be reindexed as fulltext. 345 * 346 * @param dirtyStrings set of ids, updated by this method 347 * @param dirtyBinaries set of ids, updated by this method 348 */ 349 protected void findDirtyDocuments(Set<Serializable> dirtyStrings, Set<Serializable> dirtyBinaries) { 350 // deleted documents, for which we don't need to reindex anything 351 Set<Serializable> deleted = null; 352 for (Fragment fragment : modified.values()) { 353 Serializable docId = getContainingDocument(fragment.getId()); 354 String tableName = fragment.row.tableName; 355 State state = fragment.getState(); 356 switch (state) { 357 case DELETED: 358 case DELETED_DEPENDENT: 359 if (Model.HIER_TABLE_NAME.equals(tableName) && fragment.getId().equals(docId)) { 360 // deleting the document, record this 361 if (deleted == null) { 362 deleted = new HashSet<Serializable>(); 363 } 364 deleted.add(docId); 365 } 366 if (isDeleted(docId)) { 367 break; 368 } 369 // this is a deleted fragment of a complex property 370 // from a document that has not been completely deleted 371 //$FALL-THROUGH$ 372 case CREATED: 373 PropertyType t = model.getFulltextInfoForFragment(tableName); 374 if (t == null) { 375 break; 376 } 377 if (t == PropertyType.STRING || t == PropertyType.BOOLEAN) { 378 dirtyStrings.add(docId); 379 } 380 if (t == PropertyType.BINARY || t == PropertyType.BOOLEAN) { 381 dirtyBinaries.add(docId); 382 } 383 break; 384 case MODIFIED: 385 Collection<String> keys; 386 if (model.isCollectionFragment(tableName)) { 387 keys = Collections.singleton(null); 388 } else { 389 keys = ((SimpleFragment) fragment).getDirtyKeys(); 390 } 391 for (String key : keys) { 392 PropertyType type = model.getFulltextFieldType(tableName, key); 393 if (type == PropertyType.STRING || type == PropertyType.ARRAY_STRING) { 394 dirtyStrings.add(docId); 395 } else if (type == PropertyType.BINARY || type == PropertyType.ARRAY_BINARY) { 396 dirtyBinaries.add(docId); 397 } 398 } 399 break; 400 default: 401 } 402 if (deleted != null) { 403 dirtyStrings.removeAll(deleted); 404 dirtyBinaries.removeAll(deleted); 405 } 406 } 407 } 408 409 /** 410 * Marks locally all the invalidations gathered by a {@link Mapper} operation (like a version restore). 411 */ 412 protected void markInvalidated(Invalidations invalidations) { 413 if (invalidations.modified != null) { 414 for (RowId rowId : invalidations.modified) { 415 Fragment fragment = getIfPresent(rowId); 416 if (fragment != null) { 417 setFragmentPristine(fragment); 418 fragment.setInvalidatedModified(); 419 } 420 } 421 for (SelectionContext sel : selections) { 422 sel.markInvalidated(invalidations.modified); 423 } 424 } 425 if (invalidations.deleted != null) { 426 for (RowId rowId : invalidations.deleted) { 427 Fragment fragment = getIfPresent(rowId); 428 if (fragment != null) { 429 setFragmentPristine(fragment); 430 fragment.setInvalidatedDeleted(); 431 } 432 } 433 } 434 // TODO XXX transactionInvalidations.add(invalidations); 435 } 436 437 // called from Fragment 438 protected void setFragmentModified(Fragment fragment) { 439 RowId rowId = fragment.row; 440 pristine.remove(rowId); 441 modified.put(rowId, fragment); 442 } 443 444 // also called from Fragment 445 protected void setFragmentPristine(Fragment fragment) { 446 RowId rowId = fragment.row; 447 modified.remove(rowId); 448 pristine.put(rowId, fragment); 449 } 450 451 /** 452 * Post-transaction invalidations notification. 453 * <p> 454 * Called post-transaction by session commit/rollback or transactionless save. 455 */ 456 public void sendInvalidationsToOthers() { 457 Invalidations invalidations = new Invalidations(); 458 for (SelectionContext sel : selections) { 459 sel.gatherInvalidations(invalidations); 460 } 461 mapper.sendInvalidations(invalidations); 462 } 463 464 /** 465 * Applies all invalidations accumulated. 466 * <p> 467 * Called pre-transaction by start or transactionless save; 468 */ 469 public void processReceivedInvalidations() { 470 Invalidations invals = mapper.receiveInvalidations(); 471 if (invals == null) { 472 return; 473 } 474 475 processCacheInvalidations(invals); 476 } 477 478 private void processCacheInvalidations(Invalidations invalidations) { 479 if (invalidations == null) { 480 return; 481 } 482 if (invalidations.all) { 483 clearLocalCaches(); 484 } 485 if (invalidations.modified != null) { 486 for (RowId rowId : invalidations.modified) { 487 Fragment fragment = pristine.remove(rowId); 488 if (fragment != null) { 489 fragment.setInvalidatedModified(); 490 } 491 } 492 for (SelectionContext sel : selections) { 493 sel.processReceivedInvalidations(invalidations.modified); 494 } 495 } 496 if (invalidations.deleted != null) { 497 for (RowId rowId : invalidations.deleted) { 498 Fragment fragment = pristine.remove(rowId); 499 if (fragment != null) { 500 fragment.setInvalidatedDeleted(); 501 } 502 } 503 } 504 } 505 506 public void checkInvalidationsConflict() { 507 // synchronized (receivedInvalidations) { 508 // if (receivedInvalidations.modified != null) { 509 // for (RowId rowId : receivedInvalidations.modified) { 510 // if (transactionInvalidations.contains(rowId)) { 511 // throw new ConcurrentModificationException( 512 // "Updating a concurrently modified value: " 513 // + new RowId(rowId)); 514 // } 515 // } 516 // } 517 // 518 // if (receivedInvalidations.deleted != null) { 519 // for (RowId rowId : receivedInvalidations.deleted) { 520 // if (transactionInvalidations.contains(rowId)) { 521 // throw new ConcurrentModificationException( 522 // "Updating a concurrently deleted value: " 523 // + new RowId(rowId)); 524 // } 525 // } 526 // } 527 // } 528 } 529 530 /** 531 * Gets a fragment, if present in the context. 532 * <p> 533 * Called by {@link #get}, and by the {@link Mapper} to reuse known selection fragments. 534 * 535 * @param rowId the fragment id 536 * @return the fragment, or {@code null} if not found 537 */ 538 protected Fragment getIfPresent(RowId rowId) { 539 cacheCount.inc(); 540 Fragment fragment = pristine.get(rowId); 541 if (fragment == null) { 542 fragment = modified.get(rowId); 543 } 544 if (fragment != null) { 545 cacheHitCount.inc(); 546 } 547 return fragment; 548 } 549 550 /** 551 * Gets a fragment. 552 * <p> 553 * If it's not in the context, fetch it from the mapper. If it's not in the database, returns {@code null} or an 554 * absent fragment. 555 * <p> 556 * Deleted fragments may be returned. 557 * 558 * @param rowId the fragment id 559 * @param allowAbsent {@code true} to return an absent fragment as an object instead of {@code null} 560 * @return the fragment, or {@code null} if none is found and {@value allowAbsent} was {@code false} 561 */ 562 protected Fragment get(RowId rowId, boolean allowAbsent) { 563 Fragment fragment = getIfPresent(rowId); 564 if (fragment == null) { 565 fragment = getFromMapper(rowId, allowAbsent, false); 566 } 567 return fragment; 568 } 569 570 /** 571 * Gets a fragment from the context or the mapper cache or the underlying database. 572 * 573 * @param rowId the fragment id 574 * @param allowAbsent {@code true} to return an absent fragment as an object instead of {@code null} 575 * @param cacheOnly only check memory, not the database 576 * @return the fragment, or when {@code allowAbsent} is {@code false}, a {@code null} if not found 577 */ 578 protected Fragment getFromMapper(RowId rowId, boolean allowAbsent, boolean cacheOnly) { 579 List<Fragment> fragments = getFromMapper(Collections.singleton(rowId), allowAbsent, cacheOnly); 580 return fragments.isEmpty() ? null : fragments.get(0); 581 } 582 583 /** 584 * Gets a collection of fragments from the mapper. No order is kept between the inputs and outputs. 585 * <p> 586 * Fragments not found are not returned if {@code allowAbsent} is {@code false}. 587 */ 588 protected List<Fragment> getFromMapper(Collection<RowId> rowIds, boolean allowAbsent, boolean cacheOnly) { 589 List<Fragment> res = new ArrayList<Fragment>(rowIds.size()); 590 591 // find fragments we really want to fetch 592 List<RowId> todo = new ArrayList<RowId>(rowIds.size()); 593 for (RowId rowId : rowIds) { 594 if (isIdNew(rowId.id)) { 595 // the id has not been saved, so nothing exists yet in the 596 // database 597 // rowId is not a row -> will use an absent fragment 598 Fragment fragment = getFragmentFromFetchedRow(rowId, allowAbsent); 599 if (fragment != null) { 600 res.add(fragment); 601 } 602 } else { 603 todo.add(rowId); 604 } 605 } 606 if (todo.isEmpty()) { 607 return res; 608 } 609 610 // fetch these fragments in bulk 611 List<? extends RowId> rows = mapper.read(todo, cacheOnly); 612 res.addAll(getFragmentsFromFetchedRows(rows, allowAbsent)); 613 614 return res; 615 } 616 617 /** 618 * Gets a list of fragments. 619 * <p> 620 * If a fragment is not in the context, fetch it from the mapper. If it's not in the database, use an absent 621 * fragment or skip it. 622 * <p> 623 * Deleted fragments are skipped. 624 * 625 * @param id the fragment id 626 * @param allowAbsent {@code true} to return an absent fragment as an object instead of skipping it 627 * @return the fragments, in arbitrary order (no {@code null}s) 628 */ 629 public List<Fragment> getMulti(Collection<RowId> rowIds, boolean allowAbsent) { 630 if (rowIds.isEmpty()) { 631 return Collections.emptyList(); 632 } 633 634 // find those already in the context 635 List<Fragment> res = new ArrayList<Fragment>(rowIds.size()); 636 List<RowId> todo = new LinkedList<RowId>(); 637 for (RowId rowId : rowIds) { 638 Fragment fragment = getIfPresent(rowId); 639 if (fragment == null) { 640 todo.add(rowId); 641 } else { 642 State state = fragment.getState(); 643 if (state != State.DELETED && state != State.DELETED_DEPENDENT 644 && (state != State.ABSENT || allowAbsent)) { 645 res.add(fragment); 646 } 647 } 648 } 649 if (todo.isEmpty()) { 650 return res; 651 } 652 653 // fetch missing ones, return union 654 List<Fragment> fetched = getFromMapper(todo, allowAbsent, false); 655 res.addAll(fetched); 656 return res; 657 } 658 659 /** 660 * Turns the given rows (just fetched from the mapper) into fragments and record them in the context. 661 * <p> 662 * For each row, if the context already contains a fragment with the given id, it is returned instead of building a 663 * new one. 664 * <p> 665 * Deleted fragments are skipped. 666 * <p> 667 * If a simple {@link RowId} is passed, it means that an absent row was found by the mapper. An absent fragment will 668 * be returned, unless {@code allowAbsent} is {@code false} in which case it will be skipped. 669 * 670 * @param rowIds the list of rows or row ids 671 * @param allowAbsent {@code true} to return an absent fragment as an object instead of {@code null} 672 * @return the list of fragments 673 */ 674 protected List<Fragment> getFragmentsFromFetchedRows(List<? extends RowId> rowIds, boolean allowAbsent) { 675 List<Fragment> fragments = new ArrayList<Fragment>(rowIds.size()); 676 for (RowId rowId : rowIds) { 677 Fragment fragment = getFragmentFromFetchedRow(rowId, allowAbsent); 678 if (fragment != null) { 679 fragments.add(fragment); 680 } 681 } 682 return fragments; 683 } 684 685 /** 686 * Turns the given row (just fetched from the mapper) into a fragment and record it in the context. 687 * <p> 688 * If the context already contains a fragment with the given id, it is returned instead of building a new one. 689 * <p> 690 * If the fragment was deleted, {@code null} is returned. 691 * <p> 692 * If a simple {@link RowId} is passed, it means that an absent row was found by the mapper. An absent fragment will 693 * be returned, unless {@code allowAbsent} is {@code false} in which case {@code null} will be returned. 694 * 695 * @param rowId the row or row id (may be {@code null}) 696 * @param allowAbsent {@code true} to return an absent fragment as an object instead of {@code null} 697 * @return the fragment, or {@code null} if it was deleted 698 */ 699 protected Fragment getFragmentFromFetchedRow(RowId rowId, boolean allowAbsent) { 700 if (rowId == null) { 701 return null; 702 } 703 Fragment fragment = getIfPresent(rowId); 704 if (fragment != null) { 705 // row is already known in the context, use it 706 State state = fragment.getState(); 707 if (state == State.DELETED || state == State.DELETED_DEPENDENT) { 708 // row has been deleted in the context, ignore it 709 return null; 710 } else if (state == State.INVALIDATED_MODIFIED || state == State.INVALIDATED_DELETED) { 711 // XXX TODO 712 throw new IllegalStateException(state.toString()); 713 } else { 714 // keep existing fragment 715 return fragment; 716 } 717 } 718 boolean isCollection = model.isCollectionFragment(rowId.tableName); 719 if (rowId instanceof Row) { 720 Row row = (Row) rowId; 721 if (isCollection) { 722 fragment = new CollectionFragment(row, State.PRISTINE, this); 723 } else { 724 fragment = new SimpleFragment(row, State.PRISTINE, this); 725 // add to applicable selections 726 for (SelectionContext sel : selections) { 727 if (sel.applicable((SimpleFragment) fragment)) { 728 sel.recordExisting((SimpleFragment) fragment, false); 729 } 730 } 731 } 732 return fragment; 733 } else { 734 if (allowAbsent) { 735 if (isCollection) { 736 Serializable[] empty = model.getCollectionFragmentType(rowId.tableName).getEmptyArray(); 737 Row row = new Row(rowId.tableName, rowId.id, empty); 738 return new CollectionFragment(row, State.ABSENT, this); 739 } else { 740 Row row = new Row(rowId.tableName, rowId.id); 741 return new SimpleFragment(row, State.ABSENT, this); 742 } 743 } else { 744 return null; 745 } 746 } 747 } 748 749 public SimpleFragment createHierarchyFragment(Row row) { 750 SimpleFragment fragment = createSimpleFragment(row); 751 SelectionContext hierSel = getHierSelectionContext(complexProp(fragment)); 752 hierSel.recordCreated(fragment); 753 // no children for this new node 754 Serializable id = fragment.getId(); 755 hierComplex.newSelection(id); 756 hierNonComplex.newSelection(id); 757 // could add to seriesProxies and seriesVersions as well 758 return fragment; 759 } 760 761 private SimpleFragment createVersionFragment(Row row) { 762 SimpleFragment fragment = createSimpleFragment(row); 763 seriesVersions.recordCreated(fragment); 764 // no proxies for this new version 765 if (model.proxiesEnabled) { 766 targetProxies.newSelection(fragment.getId()); 767 } 768 return fragment; 769 } 770 771 public void createdProxyFragment(SimpleFragment fragment) { 772 if (model.proxiesEnabled) { 773 seriesProxies.recordCreated(fragment); 774 targetProxies.recordCreated(fragment); 775 } 776 } 777 778 public void removedProxyTarget(SimpleFragment fragment) { 779 if (model.proxiesEnabled) { 780 targetProxies.recordRemoved(fragment); 781 } 782 } 783 784 public void addedProxyTarget(SimpleFragment fragment) { 785 if (model.proxiesEnabled) { 786 targetProxies.recordCreated(fragment); 787 } 788 } 789 790 private SimpleFragment createSimpleFragment(Row row) { 791 if (pristine.containsKey(row) || modified.containsKey(row)) { 792 throw new NuxeoException("Row already registered: " + row); 793 } 794 return new SimpleFragment(row, State.CREATED, this); 795 } 796 797 /** 798 * Removes a property node and its children. 799 * <p> 800 * There's less work to do than when we have to remove a generic document node (less selections, and we can assume 801 * the depth is small so recurse). 802 */ 803 public void removePropertyNode(SimpleFragment hierFragment) { 804 // collect children 805 Deque<SimpleFragment> todo = new LinkedList<SimpleFragment>(); 806 List<SimpleFragment> children = new LinkedList<SimpleFragment>(); 807 todo.add(hierFragment); 808 while (!todo.isEmpty()) { 809 SimpleFragment fragment = todo.removeFirst(); 810 todo.addAll(getChildren(fragment.getId(), null, true)); // complex 811 children.add(fragment); 812 } 813 Collections.reverse(children); 814 // iterate on children depth first 815 for (SimpleFragment fragment : children) { 816 // remove from context 817 boolean primary = fragment == hierFragment; 818 removeFragmentAndDependents(fragment, primary); 819 // remove from selections 820 // removed from its parent selection 821 hierComplex.recordRemoved(fragment); 822 // no children anymore 823 hierComplex.recordRemovedSelection(fragment.getId()); 824 } 825 } 826 827 private void removeFragmentAndDependents(SimpleFragment hierFragment, boolean primary) { 828 Serializable id = hierFragment.getId(); 829 for (String fragmentName : model.getTypeFragments(new IdWithTypes(hierFragment))) { 830 RowId rowId = new RowId(fragmentName, id); 831 Fragment fragment = get(rowId, true); // may read it 832 State state = fragment.getState(); 833 if (state != State.DELETED && state != State.DELETED_DEPENDENT) { 834 removeFragment(fragment, primary && hierFragment == fragment); 835 } 836 } 837 } 838 839 /** 840 * Removes a document node and its children. 841 * <p> 842 * Assumes a full flush was done. 843 */ 844 public void removeNode(SimpleFragment hierFragment) { 845 Serializable rootId = hierFragment.getId(); 846 847 // get root info before deletion. may be a version or proxy 848 SimpleFragment versionFragment; 849 SimpleFragment proxyFragment; 850 if (Model.PROXY_TYPE.equals(hierFragment.getString(Model.MAIN_PRIMARY_TYPE_KEY))) { 851 versionFragment = null; 852 proxyFragment = (SimpleFragment) get(new RowId(Model.PROXY_TABLE_NAME, rootId), true); 853 } else if (Boolean.TRUE.equals(hierFragment.get(Model.MAIN_IS_VERSION_KEY))) { 854 versionFragment = (SimpleFragment) get(new RowId(Model.VERSION_TABLE_NAME, rootId), true); 855 proxyFragment = null; 856 } else { 857 versionFragment = null; 858 proxyFragment = null; 859 } 860 NodeInfo rootInfo = new NodeInfo(hierFragment, versionFragment, proxyFragment); 861 862 // remove with descendants, and generate cache invalidations 863 List<NodeInfo> infos = mapper.remove(rootInfo); 864 865 // remove from context and selections 866 for (NodeInfo info : infos) { 867 Serializable id = info.id; 868 for (String fragmentName : model.getTypeFragments(new IdWithTypes(id, info.primaryType, null))) { 869 RowId rowId = new RowId(fragmentName, id); 870 removedFragment(rowId); // remove from context 871 } 872 removeFromSelections(info); 873 } 874 875 // recompute version series if needed 876 // only done for root of deletion as versions are not fileable 877 Serializable versionSeriesId = versionFragment == null ? null 878 : versionFragment.get(Model.VERSION_VERSIONABLE_KEY); 879 if (versionSeriesId != null) { 880 recomputeVersionSeries(versionSeriesId); 881 } 882 } 883 884 /** 885 * Remove node from children/proxies selections. 886 */ 887 private void removeFromSelections(NodeInfo info) { 888 Serializable id = info.id; 889 if (Model.PROXY_TYPE.equals(info.primaryType)) { 890 seriesProxies.recordRemoved(id, info.versionSeriesId); 891 targetProxies.recordRemoved(id, info.targetId); 892 } 893 if (info.versionSeriesId != null && info.targetId == null) { 894 // version 895 seriesVersions.recordRemoved(id, info.versionSeriesId); 896 } 897 898 hierComplex.recordRemoved(info.id, info.parentId); 899 hierNonComplex.recordRemoved(info.id, info.parentId); 900 901 // remove complete selections 902 if (complexProp(info.isProperty)) { 903 // no more a parent 904 hierComplex.recordRemovedSelection(id); 905 // is never a parent of non-complex children 906 } else { 907 // no more a parent 908 hierComplex.recordRemovedSelection(id); 909 hierNonComplex.recordRemovedSelection(id); 910 // no more a version series 911 if (model.proxiesEnabled) { 912 seriesProxies.recordRemovedSelection(id); 913 } 914 seriesVersions.recordRemovedSelection(id); 915 // no more a target 916 if (model.proxiesEnabled) { 917 targetProxies.recordRemovedSelection(id); 918 } 919 } 920 } 921 922 /** 923 * Deletes a fragment from the context. May generate a database DELETE if primary is {@code true}, otherwise 924 * consider that database removal will be a cascade-induced consequence of another DELETE. 925 */ 926 public void removeFragment(Fragment fragment, boolean primary) { 927 RowId rowId = fragment.row; 928 switch (fragment.getState()) { 929 case ABSENT: 930 case INVALIDATED_DELETED: 931 pristine.remove(rowId); 932 break; 933 case CREATED: 934 modified.remove(rowId); 935 break; 936 case PRISTINE: 937 case INVALIDATED_MODIFIED: 938 pristine.remove(rowId); 939 modified.put(rowId, fragment); 940 break; 941 case MODIFIED: 942 // already in modified 943 break; 944 case DETACHED: 945 case DELETED: 946 case DELETED_DEPENDENT: 947 break; 948 } 949 fragment.setDeleted(primary); 950 } 951 952 /** 953 * Cleans up after a fragment has been removed in the database. 954 * 955 * @param rowId the row id 956 */ 957 private void removedFragment(RowId rowId) { 958 Fragment fragment = getIfPresent(rowId); 959 if (fragment == null) { 960 return; 961 } 962 switch (fragment.getState()) { 963 case ABSENT: 964 case PRISTINE: 965 case INVALIDATED_MODIFIED: 966 case INVALIDATED_DELETED: 967 pristine.remove(rowId); 968 break; 969 case CREATED: 970 case MODIFIED: 971 case DELETED: 972 case DELETED_DEPENDENT: 973 // should not happen 974 log.error("Removed fragment is in invalid state: " + fragment); 975 modified.remove(rowId); 976 break; 977 case DETACHED: 978 break; 979 } 980 fragment.setDetached(); 981 } 982 983 /** 984 * Recomputes isLatest / isLatestMajor on all versions. 985 */ 986 public void recomputeVersionSeries(Serializable versionSeriesId) { 987 List<SimpleFragment> versFrags = seriesVersions.getSelectionFragments(versionSeriesId, null); 988 Collections.sort(versFrags, VER_CREATED_COMPARATOR); 989 Collections.reverse(versFrags); 990 boolean isLatest = true; 991 boolean isLatestMajor = true; 992 for (SimpleFragment vsf : versFrags) { 993 994 // isLatestVersion 995 vsf.put(Model.VERSION_IS_LATEST_KEY, Boolean.valueOf(isLatest)); 996 isLatest = false; 997 998 // isLatestMajorVersion 999 SimpleFragment vh = getHier(vsf.getId(), true); 1000 boolean isMajor = Long.valueOf(0).equals(vh.get(Model.MAIN_MINOR_VERSION_KEY)); 1001 vsf.put(Model.VERSION_IS_LATEST_MAJOR_KEY, Boolean.valueOf(isMajor && isLatestMajor)); 1002 if (isMajor) { 1003 isLatestMajor = false; 1004 } 1005 } 1006 } 1007 1008 /** 1009 * Gets the version ids for a version series, ordered by creation time. 1010 */ 1011 public List<Serializable> getVersionIds(Serializable versionSeriesId) { 1012 List<SimpleFragment> fragments = seriesVersions.getSelectionFragments(versionSeriesId, null); 1013 Collections.sort(fragments, VER_CREATED_COMPARATOR); 1014 return fragmentsIds(fragments); 1015 } 1016 1017 // called only when proxies enabled 1018 public List<Serializable> getSeriesProxyIds(Serializable versionSeriesId) { 1019 List<SimpleFragment> fragments = seriesProxies.getSelectionFragments(versionSeriesId, null); 1020 return fragmentsIds(fragments); 1021 } 1022 1023 // called only when proxies enabled 1024 public List<Serializable> getTargetProxyIds(Serializable targetId) { 1025 List<SimpleFragment> fragments = targetProxies.getSelectionFragments(targetId, null); 1026 return fragmentsIds(fragments); 1027 } 1028 1029 private List<Serializable> fragmentsIds(List<? extends Fragment> fragments) { 1030 List<Serializable> ids = new ArrayList<Serializable>(fragments.size()); 1031 for (Fragment fragment : fragments) { 1032 ids.add(fragment.getId()); 1033 } 1034 return ids; 1035 } 1036 1037 /* 1038 * ----- Hierarchy ----- 1039 */ 1040 1041 public static class PathAndId { 1042 public final String path; 1043 1044 public final Serializable id; 1045 1046 public PathAndId(String path, Serializable id) { 1047 this.path = path; 1048 this.id = id; 1049 } 1050 } 1051 1052 /** 1053 * Gets the path by recursing up the hierarchy. 1054 */ 1055 public String getPath(SimpleFragment hierFragment) { 1056 PathAndId pathAndId = getPathOrMissingParentId(hierFragment, true); 1057 return pathAndId.path; 1058 } 1059 1060 /** 1061 * Gets the full path, or the closest parent id which we don't have in cache. 1062 * <p> 1063 * If {@code fetch} is {@code true}, returns the full path. 1064 * <p> 1065 * If {@code fetch} is {@code false}, does not touch the mapper, only the context, therefore may return a missing 1066 * parent id instead of the path. 1067 * 1068 * @param fetch {@code true} if we can use the database, {@code false} if only caches should be used 1069 */ 1070 public PathAndId getPathOrMissingParentId(SimpleFragment hierFragment, boolean fetch) { 1071 LinkedList<String> list = new LinkedList<String>(); 1072 Serializable parentId = null; 1073 while (true) { 1074 String name = hierFragment.getString(Model.HIER_CHILD_NAME_KEY); 1075 if (name == null) { 1076 // (empty string for normal databases, null for Oracle) 1077 name = ""; 1078 } 1079 list.addFirst(name); 1080 parentId = hierFragment.get(Model.HIER_PARENT_KEY); 1081 if (parentId == null) { 1082 // root 1083 break; 1084 } 1085 // recurse in the parent 1086 RowId rowId = new RowId(Model.HIER_TABLE_NAME, parentId); 1087 hierFragment = (SimpleFragment) getIfPresent(rowId); 1088 if (hierFragment == null) { 1089 // try in mapper cache 1090 hierFragment = (SimpleFragment) getFromMapper(rowId, false, true); 1091 if (hierFragment == null) { 1092 if (!fetch) { 1093 return new PathAndId(null, parentId); 1094 } 1095 hierFragment = (SimpleFragment) getFromMapper(rowId, true, false); 1096 } 1097 } 1098 } 1099 String path; 1100 if (list.size() == 1) { 1101 String name = list.peek(); 1102 if (name.isEmpty()) { 1103 // root, special case 1104 path = "/"; 1105 } else { 1106 // placeless document, no initial slash 1107 path = name; 1108 } 1109 } else { 1110 path = StringUtils.join(list, "/"); 1111 } 1112 return new PathAndId(path, null); 1113 } 1114 1115 /** 1116 * Finds the id of the enclosing non-complex-property node. 1117 * 1118 * @param id the id 1119 * @return the id of the containing document, or {@code null} if there is no parent or the parent has been deleted. 1120 */ 1121 public Serializable getContainingDocument(Serializable id) { 1122 Serializable pid = id; 1123 while (true) { 1124 if (pid == null) { 1125 // no parent 1126 return null; 1127 } 1128 SimpleFragment p = getHier(pid, false); 1129 if (p == null) { 1130 // can happen if the fragment has been deleted 1131 return null; 1132 } 1133 if (!complexProp(p)) { 1134 return pid; 1135 } 1136 pid = p.get(Model.HIER_PARENT_KEY); 1137 } 1138 } 1139 1140 // also called by Selection 1141 protected SimpleFragment getHier(Serializable id, boolean allowAbsent) { 1142 RowId rowId = new RowId(Model.HIER_TABLE_NAME, id); 1143 return (SimpleFragment) get(rowId, allowAbsent); 1144 } 1145 1146 private boolean isOrderable(Serializable parentId, boolean complexProp) { 1147 if (complexProp) { 1148 return true; 1149 } 1150 SimpleFragment parent = getHier(parentId, true); 1151 String typeName = parent.getString(Model.MAIN_PRIMARY_TYPE_KEY); 1152 return model.getDocumentTypeFacets(typeName).contains(FacetNames.ORDERABLE); 1153 } 1154 1155 /** Recursively checks if any of a fragment's parents has been deleted. */ 1156 // needed because we don't recursively clear caches when doing a delete 1157 public boolean isDeleted(Serializable id) { 1158 while (id != null) { 1159 SimpleFragment fragment = getHier(id, false); 1160 State state; 1161 if (fragment == null || (state = fragment.getState()) == State.ABSENT || state == State.DELETED 1162 || state == State.DELETED_DEPENDENT || state == State.INVALIDATED_DELETED) { 1163 return true; 1164 } 1165 id = fragment.get(Model.HIER_PARENT_KEY); 1166 } 1167 return false; 1168 } 1169 1170 /** 1171 * Gets the next pos value for a new child in a folder. 1172 * 1173 * @param nodeId the folder node id 1174 * @param complexProp whether to deal with complex properties or regular children 1175 * @return the next pos, or {@code null} if not orderable 1176 */ 1177 public Long getNextPos(Serializable nodeId, boolean complexProp) { 1178 if (!isOrderable(nodeId, complexProp)) { 1179 return null; 1180 } 1181 long max = -1; 1182 for (SimpleFragment fragment : getChildren(nodeId, null, complexProp)) { 1183 Long pos = (Long) fragment.get(Model.HIER_CHILD_POS_KEY); 1184 if (pos != null && pos.longValue() > max) { 1185 max = pos.longValue(); 1186 } 1187 } 1188 return Long.valueOf(max + 1); 1189 } 1190 1191 /** 1192 * Order a child before another. 1193 * 1194 * @param parentId the parent id 1195 * @param sourceId the node id to move 1196 * @param destId the node id before which to place the source node, if {@code null} then move the source to the end 1197 */ 1198 public void orderBefore(Serializable parentId, Serializable sourceId, Serializable destId) { 1199 boolean complexProp = false; 1200 if (!isOrderable(parentId, complexProp)) { 1201 // TODO throw exception? 1202 return; 1203 } 1204 if (sourceId.equals(destId)) { 1205 return; 1206 } 1207 // This is optimized by assuming the number of children is small enough 1208 // to be manageable in-memory. 1209 // fetch children and relevant nodes 1210 List<SimpleFragment> fragments = getChildren(parentId, null, complexProp); 1211 // renumber fragments 1212 int i = 0; 1213 SimpleFragment source = null; // source if seen 1214 Long destPos = null; 1215 for (SimpleFragment fragment : fragments) { 1216 Serializable id = fragment.getId(); 1217 if (id.equals(destId)) { 1218 destPos = Long.valueOf(i); 1219 i++; 1220 if (source != null) { 1221 source.put(Model.HIER_CHILD_POS_KEY, destPos); 1222 } 1223 } 1224 Long setPos; 1225 if (id.equals(sourceId)) { 1226 i--; 1227 source = fragment; 1228 setPos = destPos; 1229 } else { 1230 setPos = Long.valueOf(i); 1231 } 1232 if (setPos != null) { 1233 if (!setPos.equals(fragment.get(Model.HIER_CHILD_POS_KEY))) { 1234 fragment.put(Model.HIER_CHILD_POS_KEY, setPos); 1235 } 1236 } 1237 i++; 1238 } 1239 if (destId == null) { 1240 Long setPos = Long.valueOf(i); 1241 if (!setPos.equals(source.get(Model.HIER_CHILD_POS_KEY))) { 1242 source.put(Model.HIER_CHILD_POS_KEY, setPos); 1243 } 1244 } 1245 } 1246 1247 public SimpleFragment getChildHierByName(Serializable parentId, String name, boolean complexProp) { 1248 return getHierSelectionContext(complexProp).getSelectionFragment(parentId, name); 1249 } 1250 1251 /** 1252 * Gets hier fragments for children. 1253 */ 1254 public List<SimpleFragment> getChildren(Serializable parentId, String name, boolean complexProp) { 1255 List<SimpleFragment> fragments = getHierSelectionContext(complexProp).getSelectionFragments(parentId, name); 1256 if (isOrderable(parentId, complexProp)) { 1257 // sort children in order 1258 Collections.sort(fragments, POS_COMPARATOR); 1259 } 1260 return fragments; 1261 } 1262 1263 /** Checks that we don't move/copy under ourselves. */ 1264 protected void checkNotUnder(Serializable parentId, Serializable id, String op) { 1265 Serializable pid = parentId; 1266 do { 1267 if (pid.equals(id)) { 1268 throw new DocumentExistsException("Cannot " + op + " a node under itself: " + parentId + " is under " + id); 1269 } 1270 SimpleFragment p = getHier(pid, false); 1271 if (p == null) { 1272 // cannot happen 1273 throw new NuxeoException("No parent: " + pid); 1274 } 1275 pid = p.get(Model.HIER_PARENT_KEY); 1276 } while (pid != null); 1277 } 1278 1279 /** Checks that a name is free. Cannot check concurrent sessions though. */ 1280 protected void checkFreeName(Serializable parentId, String name, boolean complexProp) { 1281 Fragment fragment = getChildHierByName(parentId, name, complexProp); 1282 if (fragment != null) { 1283 throw new DocumentExistsException("Destination name already exists: " + name); 1284 } 1285 } 1286 1287 /** 1288 * Move a child to a new parent with a new name. 1289 * 1290 * @param source the source 1291 * @param parentId the destination parent id 1292 * @param name the new name 1293 */ 1294 public void move(Node source, Serializable parentId, String name) { 1295 // a save() has already been done by the caller when doing 1296 // an actual move (different parents) 1297 Serializable id = source.getId(); 1298 SimpleFragment hierFragment = source.getHierFragment(); 1299 Serializable oldParentId = hierFragment.get(Model.HIER_PARENT_KEY); 1300 String oldName = hierFragment.getString(Model.HIER_CHILD_NAME_KEY); 1301 if (!oldParentId.equals(parentId)) { 1302 checkNotUnder(parentId, id, "move"); 1303 } else if (oldName.equals(name)) { 1304 // null move 1305 return; 1306 } 1307 boolean complexProp = complexProp(hierFragment); 1308 checkFreeName(parentId, name, complexProp); 1309 /* 1310 * Do the move. 1311 */ 1312 if (!oldName.equals(name)) { 1313 hierFragment.put(Model.HIER_CHILD_NAME_KEY, name); 1314 } 1315 // cache management 1316 getHierSelectionContext(complexProp).recordRemoved(hierFragment); 1317 hierFragment.put(Model.HIER_PARENT_KEY, parentId); 1318 getHierSelectionContext(complexProp).recordExisting(hierFragment, true); 1319 // path invalidated 1320 source.path = null; 1321 } 1322 1323 /** 1324 * Copy a child to a new parent with a new name. 1325 * 1326 * @param source the source of the copy 1327 * @param parentId the destination parent id 1328 * @param name the new name 1329 * @return the id of the copy 1330 */ 1331 public Serializable copy(Node source, Serializable parentId, String name) { 1332 Serializable id = source.getId(); 1333 SimpleFragment hierFragment = source.getHierFragment(); 1334 Serializable oldParentId = hierFragment.get(Model.HIER_PARENT_KEY); 1335 if (oldParentId != null && !oldParentId.equals(parentId)) { 1336 checkNotUnder(parentId, id, "copy"); 1337 } 1338 checkFreeName(parentId, name, complexProp(hierFragment)); 1339 // do the copy 1340 Long pos = getNextPos(parentId, false); 1341 CopyResult copyResult = mapper.copy(new IdWithTypes(source), parentId, name, null); 1342 Serializable newId = copyResult.copyId; 1343 // read new child in this session (updates children Selection) 1344 SimpleFragment copy = getHier(newId, false); 1345 // invalidate child in other sessions' children Selection 1346 markInvalidated(copyResult.invalidations); 1347 // read new proxies in this session (updates Selections) 1348 List<RowId> rowIds = new ArrayList<RowId>(); 1349 for (Serializable proxyId : copyResult.proxyIds) { 1350 rowIds.add(new RowId(Model.PROXY_TABLE_NAME, proxyId)); 1351 } 1352 // multi-fetch will register the new fragments with the Selections 1353 List<Fragment> fragments = getMulti(rowIds, true); 1354 // invalidate Selections in other sessions 1355 for (Fragment fragment : fragments) { 1356 seriesProxies.recordExisting((SimpleFragment) fragment, true); 1357 targetProxies.recordExisting((SimpleFragment) fragment, true); 1358 } 1359 // version copy fixup 1360 if (source.isVersion()) { 1361 copy.put(Model.MAIN_IS_VERSION_KEY, null); 1362 } 1363 // pos fixup 1364 copy.put(Model.HIER_CHILD_POS_KEY, pos); 1365 return newId; 1366 } 1367 1368 /** 1369 * Checks in a node (creates a version). 1370 * 1371 * @param node the node to check in 1372 * @param label the version label 1373 * @param checkinComment the version description 1374 * @return the created version id 1375 */ 1376 public Serializable checkIn(Node node, String label, String checkinComment) { 1377 Boolean checkedIn = (Boolean) node.hierFragment.get(Model.MAIN_CHECKED_IN_KEY); 1378 if (Boolean.TRUE.equals(checkedIn)) { 1379 throw new NuxeoException("Already checked in"); 1380 } 1381 if (label == null) { 1382 // use version major + minor as label 1383 try { 1384 Serializable major = node.getSimpleProperty(Model.MAIN_MAJOR_VERSION_PROP).getValue(); 1385 Serializable minor = node.getSimpleProperty(Model.MAIN_MINOR_VERSION_PROP).getValue(); 1386 if (major == null || minor == null) { 1387 label = ""; 1388 } else { 1389 label = major + "." + minor; 1390 } 1391 } catch (IllegalArgumentException e) { 1392 log.error("Cannot get version", e); 1393 label = ""; 1394 } 1395 } 1396 1397 /* 1398 * Do the copy without non-complex children, with null parent. 1399 */ 1400 Serializable id = node.getId(); 1401 CopyResult res = mapper.copy(new IdWithTypes(node), null, null, null); 1402 Serializable newId = res.copyId; 1403 markInvalidated(res.invalidations); 1404 // add version as a new child of its parent 1405 SimpleFragment verHier = getHier(newId, false); 1406 verHier.put(Model.MAIN_IS_VERSION_KEY, Boolean.TRUE); 1407 boolean isMajor = Long.valueOf(0).equals(verHier.get(Model.MAIN_MINOR_VERSION_KEY)); 1408 1409 // create a "version" row for our new version 1410 Row row = new Row(Model.VERSION_TABLE_NAME, newId); 1411 row.putNew(Model.VERSION_VERSIONABLE_KEY, id); 1412 row.putNew(Model.VERSION_CREATED_KEY, new GregorianCalendar()); // now 1413 row.putNew(Model.VERSION_LABEL_KEY, label); 1414 row.putNew(Model.VERSION_DESCRIPTION_KEY, checkinComment); 1415 row.putNew(Model.VERSION_IS_LATEST_KEY, Boolean.TRUE); 1416 row.putNew(Model.VERSION_IS_LATEST_MAJOR_KEY, Boolean.valueOf(isMajor)); 1417 createVersionFragment(row); 1418 1419 // update the original node to reflect that it's checked in 1420 node.hierFragment.put(Model.MAIN_CHECKED_IN_KEY, Boolean.TRUE); 1421 node.hierFragment.put(Model.MAIN_BASE_VERSION_KEY, newId); 1422 1423 recomputeVersionSeries(id); 1424 1425 return newId; 1426 } 1427 1428 /** 1429 * Checks out a node. 1430 * 1431 * @param node the node to check out 1432 */ 1433 public void checkOut(Node node) { 1434 Boolean checkedIn = (Boolean) node.hierFragment.get(Model.MAIN_CHECKED_IN_KEY); 1435 if (!Boolean.TRUE.equals(checkedIn)) { 1436 throw new NuxeoException("Already checked out"); 1437 } 1438 // update the node to reflect that it's checked out 1439 node.hierFragment.put(Model.MAIN_CHECKED_IN_KEY, Boolean.FALSE); 1440 } 1441 1442 /** 1443 * Restores a node to a given version. 1444 * <p> 1445 * The restored node is checked in. 1446 * 1447 * @param node the node 1448 * @param version the version to restore on this node 1449 */ 1450 public void restoreVersion(Node node, Node version) { 1451 Serializable versionableId = node.getId(); 1452 Serializable versionId = version.getId(); 1453 1454 // clear complex properties 1455 List<SimpleFragment> children = getChildren(versionableId, null, true); 1456 // copy to avoid concurrent modifications 1457 for (SimpleFragment child : children.toArray(new SimpleFragment[children.size()])) { 1458 removePropertyNode(child); 1459 } 1460 session.flush(); // flush deletes 1461 1462 // copy the version values 1463 Row overwriteRow = new Row(Model.HIER_TABLE_NAME, versionableId); 1464 SimpleFragment versionHier = version.getHierFragment(); 1465 for (String key : model.getFragmentKeysType(Model.HIER_TABLE_NAME).keySet()) { 1466 // keys we don't copy from version when restoring 1467 if (key.equals(Model.HIER_PARENT_KEY) || key.equals(Model.HIER_CHILD_NAME_KEY) 1468 || key.equals(Model.HIER_CHILD_POS_KEY) || key.equals(Model.HIER_CHILD_ISPROPERTY_KEY) 1469 || key.equals(Model.MAIN_PRIMARY_TYPE_KEY) || key.equals(Model.MAIN_CHECKED_IN_KEY) 1470 || key.equals(Model.MAIN_BASE_VERSION_KEY) || key.equals(Model.MAIN_IS_VERSION_KEY)) { 1471 continue; 1472 } 1473 overwriteRow.putNew(key, versionHier.get(key)); 1474 } 1475 overwriteRow.putNew(Model.MAIN_CHECKED_IN_KEY, Boolean.TRUE); 1476 overwriteRow.putNew(Model.MAIN_BASE_VERSION_KEY, versionId); 1477 overwriteRow.putNew(Model.MAIN_IS_VERSION_KEY, null); 1478 CopyResult res = mapper.copy(new IdWithTypes(version), node.getParentId(), null, overwriteRow); 1479 markInvalidated(res.invalidations); 1480 } 1481 1482}