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