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                RowUpdate rowu = fragment.getRowUpdate();
294                if (rowu != null) {
295                    batch.updates.add(rowu);
296                    fragmentsToClearDirty.add(fragment);
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            Serializable major = node.getSimpleProperty(Model.MAIN_MAJOR_VERSION_PROP).getValue();
1384            Serializable minor = node.getSimpleProperty(Model.MAIN_MINOR_VERSION_PROP).getValue();
1385            if (major == null) {
1386                major = "0";
1387            }
1388            if (minor == null) {
1389                minor = "0";
1390            }
1391            label = major + "." + minor;
1392        }
1393
1394        /*
1395         * Do the copy without non-complex children, with null parent.
1396         */
1397        Serializable id = node.getId();
1398        CopyResult res = mapper.copy(new IdWithTypes(node), null, null, null);
1399        Serializable newId = res.copyId;
1400        markInvalidated(res.invalidations);
1401        // add version as a new child of its parent
1402        SimpleFragment verHier = getHier(newId, false);
1403        verHier.put(Model.MAIN_IS_VERSION_KEY, Boolean.TRUE);
1404        boolean isMajor = Long.valueOf(0).equals(verHier.get(Model.MAIN_MINOR_VERSION_KEY));
1405
1406        // create a "version" row for our new version
1407        Row row = new Row(Model.VERSION_TABLE_NAME, newId);
1408        row.putNew(Model.VERSION_VERSIONABLE_KEY, id);
1409        row.putNew(Model.VERSION_CREATED_KEY, new GregorianCalendar()); // now
1410        row.putNew(Model.VERSION_LABEL_KEY, label);
1411        row.putNew(Model.VERSION_DESCRIPTION_KEY, checkinComment);
1412        row.putNew(Model.VERSION_IS_LATEST_KEY, Boolean.TRUE);
1413        row.putNew(Model.VERSION_IS_LATEST_MAJOR_KEY, Boolean.valueOf(isMajor));
1414        createVersionFragment(row);
1415
1416        // update the original node to reflect that it's checked in
1417        node.hierFragment.put(Model.MAIN_CHECKED_IN_KEY, Boolean.TRUE);
1418        node.hierFragment.put(Model.MAIN_BASE_VERSION_KEY, newId);
1419
1420        recomputeVersionSeries(id);
1421
1422        return newId;
1423    }
1424
1425    /**
1426     * Checks out a node.
1427     *
1428     * @param node the node to check out
1429     */
1430    public void checkOut(Node node) {
1431        Boolean checkedIn = (Boolean) node.hierFragment.get(Model.MAIN_CHECKED_IN_KEY);
1432        if (!Boolean.TRUE.equals(checkedIn)) {
1433            throw new NuxeoException("Already checked out");
1434        }
1435        // update the node to reflect that it's checked out
1436        node.hierFragment.put(Model.MAIN_CHECKED_IN_KEY, Boolean.FALSE);
1437    }
1438
1439    /**
1440     * Restores a node to a given version.
1441     * <p>
1442     * The restored node is checked in.
1443     *
1444     * @param node the node
1445     * @param version the version to restore on this node
1446     */
1447    public void restoreVersion(Node node, Node version) {
1448        Serializable versionableId = node.getId();
1449        Serializable versionId = version.getId();
1450
1451        // clear complex properties
1452        List<SimpleFragment> children = getChildren(versionableId, null, true);
1453        // copy to avoid concurrent modifications
1454        for (SimpleFragment child : children.toArray(new SimpleFragment[children.size()])) {
1455            removePropertyNode(child);
1456        }
1457        session.flush(); // flush deletes
1458
1459        // copy the version values
1460        Row overwriteRow = new Row(Model.HIER_TABLE_NAME, versionableId);
1461        SimpleFragment versionHier = version.getHierFragment();
1462        for (String key : model.getFragmentKeysType(Model.HIER_TABLE_NAME).keySet()) {
1463            // keys we don't copy from version when restoring
1464            if (key.equals(Model.HIER_PARENT_KEY) || key.equals(Model.HIER_CHILD_NAME_KEY)
1465                    || key.equals(Model.HIER_CHILD_POS_KEY) || key.equals(Model.HIER_CHILD_ISPROPERTY_KEY)
1466                    || key.equals(Model.MAIN_PRIMARY_TYPE_KEY) || key.equals(Model.MAIN_CHECKED_IN_KEY)
1467                    || key.equals(Model.MAIN_BASE_VERSION_KEY) || key.equals(Model.MAIN_IS_VERSION_KEY)) {
1468                continue;
1469            }
1470            overwriteRow.putNew(key, versionHier.get(key));
1471        }
1472        overwriteRow.putNew(Model.MAIN_CHECKED_IN_KEY, Boolean.TRUE);
1473        overwriteRow.putNew(Model.MAIN_BASE_VERSION_KEY, versionId);
1474        overwriteRow.putNew(Model.MAIN_IS_VERSION_KEY, null);
1475        CopyResult res = mapper.copy(new IdWithTypes(version), node.getParentId(), null, overwriteRow);
1476        markInvalidated(res.invalidations);
1477    }
1478
1479}