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