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