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