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