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