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}