001/*
002 * (C) Copyright 2006-2017 Nuxeo (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.text.Normalizer;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Calendar;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Iterator;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Map.Entry;
035import java.util.stream.Collectors;
036import java.util.Set;
037
038import javax.resource.ResourceException;
039import javax.resource.cci.ConnectionMetaData;
040import javax.resource.cci.Interaction;
041import javax.resource.cci.LocalTransaction;
042import javax.resource.cci.ResultSetInfo;
043import javax.transaction.xa.XAException;
044import javax.transaction.xa.XAResource;
045import javax.transaction.xa.Xid;
046
047import org.apache.commons.lang.StringUtils;
048import org.apache.commons.logging.Log;
049import org.apache.commons.logging.LogFactory;
050import org.nuxeo.ecm.core.api.ConcurrentUpdateException;
051import org.nuxeo.ecm.core.api.DocumentExistsException;
052import org.nuxeo.ecm.core.api.IterableQueryResult;
053import org.nuxeo.ecm.core.api.NuxeoException;
054import org.nuxeo.ecm.core.api.PartialList;
055import org.nuxeo.ecm.core.api.ScrollResult;
056import org.nuxeo.ecm.core.api.repository.RepositoryManager;
057import org.nuxeo.ecm.core.api.security.ACL;
058import org.nuxeo.ecm.core.api.security.SecurityConstants;
059import org.nuxeo.ecm.core.model.LockManager;
060import org.nuxeo.ecm.core.query.QueryFilter;
061import org.nuxeo.ecm.core.query.sql.NXQL;
062import org.nuxeo.ecm.core.schema.DocumentType;
063import org.nuxeo.ecm.core.schema.SchemaManager;
064import org.nuxeo.ecm.core.storage.FulltextConfiguration;
065import org.nuxeo.ecm.core.storage.FulltextParser;
066import org.nuxeo.ecm.core.storage.FulltextUpdaterWork;
067import org.nuxeo.ecm.core.storage.FulltextUpdaterWork.IndexAndText;
068import org.nuxeo.ecm.core.storage.sql.PersistenceContext.PathAndId;
069import org.nuxeo.ecm.core.storage.sql.RowMapper.NodeInfo;
070import org.nuxeo.ecm.core.storage.sql.RowMapper.RowBatch;
071import org.nuxeo.ecm.core.storage.sql.coremodel.SQLFulltextExtractorWork;
072import org.nuxeo.ecm.core.work.api.Work;
073import org.nuxeo.ecm.core.work.api.WorkManager;
074import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling;
075import org.nuxeo.runtime.api.Framework;
076import org.nuxeo.runtime.metrics.MetricsService;
077import org.nuxeo.runtime.transaction.TransactionHelper;
078
079import com.codahale.metrics.MetricRegistry;
080import com.codahale.metrics.SharedMetricRegistries;
081import com.codahale.metrics.Timer;
082
083/**
084 * The session is the main high level access point to data from the underlying database.
085 */
086public class SessionImpl implements Session, XAResource {
087
088    private static final Log log = LogFactory.getLog(SessionImpl.class);
089
090    /**
091     * Set this system property to false if you don't want repositories to be looked up under the compatibility name
092     * "default" in the "repositories" table.
093     * <p>
094     * Only do this if you start from an empty database, or if you have migrated the "repositories" table by hand, or if
095     * you need to create a new repository in a database already containing a "default" repository (table sharing, not
096     * recommended).
097     */
098    public static final String COMPAT_REPOSITORY_NAME_KEY = "org.nuxeo.vcs.repository.name.default.compat";
099
100    private static final boolean COMPAT_REPOSITORY_NAME = Boolean.parseBoolean(
101            Framework.getProperty(COMPAT_REPOSITORY_NAME_KEY, "true"));
102
103    protected final RepositoryImpl repository;
104
105    private final Mapper mapper;
106
107    private final Model model;
108
109    protected final FulltextParser fulltextParser;
110
111    // public because used by unit tests
112    public final PersistenceContext context;
113
114    protected final boolean changeTokenEnabled;
115
116    private volatile boolean live;
117
118    private boolean inTransaction;
119
120    private Node rootNode;
121
122    private long threadId;
123
124    private String threadName;
125
126    private Throwable threadStack;
127
128    private boolean readAclsChanged;
129
130    // @since 5.7
131    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
132
133    private final Timer saveTimer;
134
135    private final Timer queryTimer;
136
137    private final Timer aclrUpdateTimer;
138
139    private static final java.lang.String LOG_MIN_DURATION_KEY = "org.nuxeo.vcs.query.log_min_duration_ms";
140
141    private static final long LOG_MIN_DURATION_NS = Long.parseLong(Framework.getProperty(LOG_MIN_DURATION_KEY, "-1"))
142            * 1000000;
143
144    public SessionImpl(RepositoryImpl repository, Model model, Mapper mapper) {
145        this.repository = repository;
146        this.mapper = mapper;
147        this.model = model;
148        context = new PersistenceContext(model, mapper, this);
149        changeTokenEnabled = repository.isChangeTokenEnabled();
150        live = true;
151        readAclsChanged = false;
152
153        try {
154            fulltextParser = repository.fulltextParserClass.newInstance();
155        } catch (ReflectiveOperationException e) {
156            throw new NuxeoException(e);
157        }
158        saveTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), "saves"));
159        queryTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), "queries"));
160        aclrUpdateTimer = registry.timer(
161                MetricRegistry.name("nuxeo", "repositories", repository.getName(), "aclr-updates"));
162
163        computeRootNode();
164    }
165
166    public void checkLive() {
167        if (!live) {
168            throw new IllegalStateException("Session is not live");
169        }
170        checkThread();
171    }
172
173    // called by NetServlet when forwarding remote NetMapper calls.
174    @Override
175    public Mapper getMapper() {
176        return mapper;
177    }
178
179    /**
180     * Gets the XAResource. Called by the ManagedConnectionImpl, which actually wraps it in a connection-aware
181     * implementation.
182     */
183    public XAResource getXAResource() {
184        return this;
185    }
186
187    /**
188     * Clears all the caches. Called by RepositoryManagement.
189     */
190    protected int clearCaches() {
191        if (inTransaction) {
192            // avoid potential multi-threaded access to active session
193            return 0;
194        }
195        checkThreadEnd();
196        return context.clearCaches();
197    }
198
199    protected PersistenceContext getContext() {
200        return context;
201    }
202
203    protected void rollback() {
204        context.clearCaches();
205    }
206
207    protected void checkThread() {
208        if (threadId == 0) {
209            return;
210        }
211        long currentThreadId = Thread.currentThread().getId();
212        if (threadId == currentThreadId) {
213            return;
214        }
215        String currentThreadName = Thread.currentThread().getName();
216        String msg = String.format(
217                "Concurrency Error: Session was started in thread %s (%s)" + " but is being used in thread %s (%s)",
218                threadId, threadName, currentThreadId, currentThreadName);
219        throw new IllegalStateException(msg, threadStack);
220    }
221
222    protected void checkThreadStart() {
223        threadId = Thread.currentThread().getId();
224        threadName = Thread.currentThread().getName();
225        if (log.isDebugEnabled()) {
226            threadStack = new Throwable("owner stack trace");
227        }
228    }
229
230    protected void checkThreadEnd() {
231        threadId = 0;
232        threadName = null;
233        threadStack = null;
234    }
235
236    /**
237     * Generates a new id, or used a pre-generated one (import).
238     */
239    protected Serializable generateNewId(Serializable id) {
240        return context.generateNewId(id);
241    }
242
243    protected boolean isIdNew(Serializable id) {
244        return context.isIdNew(id);
245    }
246
247    /*
248     * ----- javax.resource.cci.Connection -----
249     */
250
251    @Override
252    public void close() throws ResourceException {
253        try {
254            checkLive();
255            closeSession();
256            repository.closeSession(this);
257        } catch (Exception cause) {
258            throw new ResourceException(cause);
259        }
260    }
261
262    protected void closeSession() {
263        live = false;
264        context.clearCaches();
265        // close the mapper and therefore the connection
266        mapper.close();
267        // don't clean the caches, we keep the pristine cache around
268        // TODO this is getting destroyed, we can clean everything
269    }
270
271    @Override
272    public Interaction createInteraction() throws ResourceException {
273        throw new UnsupportedOperationException();
274    }
275
276    @Override
277    public LocalTransaction getLocalTransaction() throws ResourceException {
278        throw new UnsupportedOperationException();
279    }
280
281    @Override
282    public ConnectionMetaData getMetaData() throws ResourceException {
283        throw new UnsupportedOperationException();
284    }
285
286    @Override
287    public ResultSetInfo getResultSetInfo() throws ResourceException {
288        throw new UnsupportedOperationException();
289    }
290
291    /*
292     * ----- Session -----
293     */
294
295    @Override
296    public boolean isLive() {
297        return live;
298    }
299
300    @Override
301    public String getRepositoryName() {
302        return repository.getName();
303    }
304
305    @Override
306    public Model getModel() {
307        return model;
308    }
309
310    @Override
311    public Node getRootNode() {
312        checkLive();
313        return rootNode;
314    }
315
316    @Override
317    public void save() {
318        final Timer.Context timerContext = saveTimer.time();
319        try {
320            checkLive();
321            flush();
322            if (!inTransaction) {
323                sendInvalidationsToOthers();
324                // as we don't have a way to know when the next
325                // non-transactional
326                // statement will start, process invalidations immediately
327            }
328            processReceivedInvalidations();
329        } finally {
330            timerContext.stop();
331        }
332    }
333
334    protected void flush() {
335        checkThread();
336        List<Work> works;
337        if (!repository.getRepositoryDescriptor().getFulltextDescriptor().getFulltextDisabled()) {
338            works = getFulltextWorks();
339        } else {
340            works = Collections.emptyList();
341        }
342        doFlush();
343        if (readAclsChanged) {
344            updateReadAcls();
345        }
346        scheduleWork(works);
347        checkInvalidationsConflict();
348    }
349
350    protected void scheduleWork(List<Work> works) {
351        // do async fulltext indexing only if high-level sessions are available
352        RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class);
353        if (repositoryManager != null && !works.isEmpty()) {
354            WorkManager workManager = Framework.getLocalService(WorkManager.class);
355            for (Work work : works) {
356                // schedule work post-commit
357                // in non-tx mode, this may execute it nearly immediately
358                workManager.schedule(work, Scheduling.IF_NOT_SCHEDULED, true);
359            }
360        }
361    }
362
363    protected void doFlush() {
364        List<Fragment> fragmentsToClearDirty = new ArrayList<>(0);
365        RowBatch batch = context.getSaveBatch(fragmentsToClearDirty);
366        if (!batch.isEmpty()) {
367            log.debug("Saving session");
368            // execute the batch
369            mapper.write(batch);
370            log.debug("End of save");
371            for (Fragment fragment : fragmentsToClearDirty) {
372                fragment.clearDirty();
373            }
374        }
375    }
376
377    protected Serializable getContainingDocument(Serializable id) {
378        return context.getContainingDocument(id);
379    }
380
381    /**
382     * Gets the fulltext updates to do. Called at save() time.
383     *
384     * @return a list of {@link Work} instances to schedule post-commit.
385     */
386    protected List<Work> getFulltextWorks() {
387        Set<Serializable> dirtyStrings = new HashSet<>();
388        Set<Serializable> dirtyBinaries = new HashSet<>();
389        context.findDirtyDocuments(dirtyStrings, dirtyBinaries);
390        if (dirtyStrings.isEmpty() && dirtyBinaries.isEmpty()) {
391            return Collections.emptyList();
392        }
393
394        List<Work> works = new LinkedList<>();
395        getFulltextSimpleWorks(works, dirtyStrings);
396        getFulltextBinariesWorks(works, dirtyBinaries);
397        return works;
398    }
399
400    protected void getFulltextSimpleWorks(List<Work> works, Set<Serializable> dirtyStrings) {
401        FulltextConfiguration fulltextConfiguration = model.getFulltextConfiguration();
402        if (fulltextConfiguration.fulltextSearchDisabled) {
403            return;
404        }
405        // update simpletext on documents with dirty strings
406        for (Serializable docId : dirtyStrings) {
407            if (docId == null) {
408                // cannot happen, but has been observed :(
409                log.error("Got null doc id in fulltext update, cannot happen");
410                continue;
411            }
412            Node document = getNodeById(docId);
413            if (document == null) {
414                // cannot happen
415                continue;
416            }
417            if (document.isProxy()) {
418                // proxies don't have any fulltext attached, it's
419                // the target document that carries it
420                continue;
421            }
422            String documentType = document.getPrimaryType();
423            String[] mixinTypes = document.getMixinTypes();
424
425            if (!fulltextConfiguration.isFulltextIndexable(documentType)) {
426                continue;
427            }
428            document.getSimpleProperty(Model.FULLTEXT_JOBID_PROP).setValue(model.idToString(document.getId()));
429            FulltextFinder fulltextFinder = new FulltextFinder(fulltextParser, document, this);
430            List<IndexAndText> indexesAndText = new LinkedList<>();
431            for (String indexName : fulltextConfiguration.indexNames) {
432                Set<String> paths;
433                if (fulltextConfiguration.indexesAllSimple.contains(indexName)) {
434                    // index all string fields, minus excluded ones
435                    // TODO XXX excluded ones...
436                    paths = model.getSimpleTextPropertyPaths(documentType, mixinTypes);
437                } else {
438                    // index configured fields
439                    paths = fulltextConfiguration.propPathsByIndexSimple.get(indexName);
440                }
441                String text = fulltextFinder.findFulltext(paths);
442                indexesAndText.add(new IndexAndText(indexName, text));
443            }
444            if (!indexesAndText.isEmpty()) {
445                Work work = new FulltextUpdaterWork(repository.getName(), model.idToString(docId), true, false,
446                        indexesAndText);
447                works.add(work);
448            }
449        }
450    }
451
452    protected void getFulltextBinariesWorks(List<Work> works, final Set<Serializable> dirtyBinaries) {
453        if (dirtyBinaries.isEmpty()) {
454            return;
455        }
456
457        // mark indexing in progress, so that future copies (including versions)
458        // will be indexed as well
459        for (Node node : getNodesByIds(new ArrayList<>(dirtyBinaries))) {
460            if (!model.getFulltextConfiguration().isFulltextIndexable(node.getPrimaryType())) {
461                continue;
462            }
463            node.getSimpleProperty(Model.FULLTEXT_JOBID_PROP).setValue(model.idToString(node.getId()));
464        }
465
466        // FulltextExtractorWork does fulltext extraction using converters
467        // and then schedules a FulltextUpdaterWork to write the results
468        // single-threaded
469        for (Serializable id : dirtyBinaries) {
470            String docId = model.idToString(id);
471            Work work = new SQLFulltextExtractorWork(repository.getName(), docId);
472            works.add(work);
473        }
474    }
475
476    /**
477     * Finds the fulltext in a document and sends it to a fulltext parser.
478     *
479     * @since 5.9.5
480     */
481    protected static class FulltextFinder {
482
483        protected final FulltextParser fulltextParser;
484
485        protected final Node document;
486
487        protected final SessionImpl session;
488
489        protected final String documentType;
490
491        protected final String[] mixinTypes;
492
493        public FulltextFinder(FulltextParser fulltextParser, Node document, SessionImpl session) {
494            this.fulltextParser = fulltextParser;
495            this.document = document;
496            this.session = session;
497            if (document == null) {
498                documentType = null;
499                mixinTypes = null;
500            } else { // null in tests
501                documentType = document.getPrimaryType();
502                mixinTypes = document.getMixinTypes();
503            }
504        }
505
506        /**
507         * Parses the document for one index.
508         */
509        protected String findFulltext(Set<String> paths) {
510            if (paths == null) {
511                return "";
512            }
513            List<String> strings = new ArrayList<>();
514
515            for (String path : paths) {
516                ModelProperty pi = session.getModel().getPathPropertyInfo(documentType, mixinTypes, path);
517                if (pi == null) {
518                    continue; // doc type doesn't have this property
519                }
520                if (pi.propertyType != PropertyType.STRING && pi.propertyType != PropertyType.ARRAY_STRING) {
521                    continue;
522                }
523
524                List<Node> nodes = new ArrayList<>(Collections.singleton(document));
525
526                String[] names = path.split("/");
527                for (int i = 0; i < names.length; i++) {
528                    String name = names[i];
529                    if (i < names.length - 1) {
530                        // traverse
531                        List<Node> newNodes;
532                        if ("*".equals(names[i + 1])) {
533                            // traverse complex list
534                            i++;
535                            newNodes = new ArrayList<>();
536                            for (Node node : nodes) {
537                                newNodes.addAll(session.getChildren(node, name, true));
538                            }
539                        } else {
540                            // traverse child
541                            newNodes = new ArrayList<>(nodes.size());
542                            for (Node node : nodes) {
543                                node = session.getChildNode(node, name, true);
544                                if (node != null) {
545                                    newNodes.add(node);
546                                }
547                            }
548                        }
549                        nodes = newNodes;
550                    } else {
551                        // last path component: get value
552                        for (Node node : nodes) {
553                            if (pi.propertyType == PropertyType.STRING) {
554                                String v = node.getSimpleProperty(name).getString();
555                                if (v != null) {
556                                    fulltextParser.parse(v, path, strings);
557                                }
558                            } else { /* ARRAY_STRING */
559                                for (Serializable v : node.getCollectionProperty(name).getValue()) {
560                                    if (v != null) {
561                                        fulltextParser.parse((String) v, path, strings);
562                                    }
563                                }
564                            }
565                        }
566                    }
567                }
568            }
569            return StringUtils.join(strings, ' ');
570        }
571    }
572
573    /**
574     * Post-transaction invalidations notification.
575     * <p>
576     * Called post-transaction by session commit/rollback or transactionless save.
577     */
578    protected void sendInvalidationsToOthers() {
579        context.sendInvalidationsToOthers();
580    }
581
582    /**
583     * Processes all invalidations accumulated.
584     * <p>
585     * Called pre-transaction by start or transactionless save;
586     */
587    protected void processReceivedInvalidations() {
588        context.processReceivedInvalidations();
589    }
590
591    /**
592     * Post transaction check invalidations processing.
593     */
594    protected void checkInvalidationsConflict() {
595        // repository.receiveClusterInvalidations(this);
596        context.checkInvalidationsConflict();
597    }
598
599    /*
600     * -------------------------------------------------------------
601     * -------------------------------------------------------------
602     * -------------------------------------------------------------
603     */
604
605    protected Node getNodeById(Serializable id, boolean prefetch) {
606        List<Node> nodes = getNodesByIds(Collections.singletonList(id), prefetch);
607        Node node = nodes.get(0);
608        // ((JDBCMapper) ((CachingMapper)
609        // mapper).mapper).logger.log("getNodeById " + id + " -> " + (node ==
610        // null ? "missing" : "found"));
611        return node;
612    }
613
614    @Override
615    public Node getNodeById(Serializable id) {
616        checkLive();
617        if (id == null) {
618            throw new IllegalArgumentException("Illegal null id");
619        }
620        return getNodeById(id, true);
621    }
622
623    public List<Node> getNodesByIds(List<Serializable> ids, boolean prefetch) {
624        // get hier fragments
625        List<RowId> hierRowIds = new ArrayList<>(ids.size());
626        for (Serializable id : ids) {
627            hierRowIds.add(new RowId(Model.HIER_TABLE_NAME, id));
628        }
629
630        List<Fragment> hierFragments = context.getMulti(hierRowIds, false);
631
632        // find available paths
633        Map<Serializable, String> paths = new HashMap<>();
634        Set<Serializable> parentIds = new HashSet<>();
635        for (Fragment fragment : hierFragments) {
636            Serializable id = fragment.getId();
637            PathAndId pathOrId = context.getPathOrMissingParentId((SimpleFragment) fragment, false);
638            // find missing fragments
639            if (pathOrId.path != null) {
640                paths.put(id, pathOrId.path);
641            } else {
642                parentIds.add(pathOrId.id);
643            }
644        }
645        // fetch the missing parents and their ancestors in bulk
646        if (!parentIds.isEmpty()) {
647            // fetch them in the context
648            getHierarchyAndAncestors(parentIds);
649            // compute missing paths using context
650            for (Fragment fragment : hierFragments) {
651                Serializable id = fragment.getId();
652                if (paths.containsKey(id)) {
653                    continue;
654                }
655                String path = context.getPath((SimpleFragment) fragment);
656                paths.put(id, path);
657            }
658        }
659
660        // prepare fragment groups to build nodes
661        Map<Serializable, FragmentGroup> fragmentGroups = new HashMap<>(ids.size());
662        for (Fragment fragment : hierFragments) {
663            Serializable id = fragment.row.id;
664            fragmentGroups.put(id, new FragmentGroup((SimpleFragment) fragment, new FragmentsMap()));
665        }
666
667        if (prefetch) {
668            List<RowId> bulkRowIds = new ArrayList<>();
669            Set<Serializable> proxyIds = new HashSet<>();
670
671            // get rows to prefetch for hier fragments
672            for (Fragment fragment : hierFragments) {
673                findPrefetchedFragments((SimpleFragment) fragment, bulkRowIds, proxyIds);
674            }
675
676            // proxies
677
678            // get proxies fragments
679            List<RowId> proxiesRowIds = new ArrayList<>(proxyIds.size());
680            for (Serializable id : proxyIds) {
681                proxiesRowIds.add(new RowId(Model.PROXY_TABLE_NAME, id));
682            }
683            List<Fragment> proxiesFragments = context.getMulti(proxiesRowIds, true);
684            Set<Serializable> targetIds = new HashSet<>();
685            for (Fragment fragment : proxiesFragments) {
686                Serializable targetId = ((SimpleFragment) fragment).get(Model.PROXY_TARGET_KEY);
687                targetIds.add(targetId);
688            }
689
690            // get hier fragments for proxies' targets
691            targetIds.removeAll(ids); // only those we don't have already
692            hierRowIds = new ArrayList<>(targetIds.size());
693            for (Serializable id : targetIds) {
694                hierRowIds.add(new RowId(Model.HIER_TABLE_NAME, id));
695            }
696            hierFragments = context.getMulti(hierRowIds, true);
697            for (Fragment fragment : hierFragments) {
698                findPrefetchedFragments((SimpleFragment) fragment, bulkRowIds, null);
699            }
700
701            // we have everything to be prefetched
702
703            // fetch all the prefetches in bulk
704            List<Fragment> fragments = context.getMulti(bulkRowIds, true);
705
706            // put each fragment in the map of the proper group
707            for (Fragment fragment : fragments) {
708                FragmentGroup fragmentGroup = fragmentGroups.get(fragment.row.id);
709                if (fragmentGroup != null) {
710                    fragmentGroup.fragments.put(fragment.row.tableName, fragment);
711                }
712            }
713        }
714
715        // assemble nodes from the fragment groups
716        List<Node> nodes = new ArrayList<>(ids.size());
717        for (Serializable id : ids) {
718            FragmentGroup fragmentGroup = fragmentGroups.get(id);
719            // null if deleted/absent
720            Node node = fragmentGroup == null ? null : new Node(context, fragmentGroup, paths.get(id));
721            nodes.add(node);
722        }
723
724        return nodes;
725    }
726
727    /**
728     * Finds prefetched fragments for a hierarchy fragment, takes note of the ones that are proxies.
729     */
730    protected void findPrefetchedFragments(SimpleFragment hierFragment, List<RowId> bulkRowIds,
731            Set<Serializable> proxyIds) {
732        Serializable id = hierFragment.row.id;
733
734        // find type
735        String typeName = (String) hierFragment.get(Model.MAIN_PRIMARY_TYPE_KEY);
736        if (Model.PROXY_TYPE.equals(typeName)) {
737            if (proxyIds != null) {
738                proxyIds.add(id);
739            }
740            return;
741        }
742
743        // find table names
744        Set<String> tableNames = model.getTypePrefetchedFragments(typeName);
745        if (tableNames == null) {
746            return; // unknown (obsolete) type
747        }
748
749        // add row id for each table name
750        Serializable parentId = hierFragment.get(Model.HIER_PARENT_KEY);
751        for (String tableName : tableNames) {
752            if (Model.HIER_TABLE_NAME.equals(tableName)) {
753                continue; // already fetched
754            }
755            if (parentId != null && Model.VERSION_TABLE_NAME.equals(tableName)) {
756                continue; // not a version, don't fetch this table
757                // TODO incorrect if we have filed versions
758            }
759            bulkRowIds.add(new RowId(tableName, id));
760        }
761    }
762
763    @Override
764    public List<Node> getNodesByIds(List<Serializable> ids) {
765        checkLive();
766        return getNodesByIds(ids, true);
767    }
768
769    @Override
770    public Node getParentNode(Node node) {
771        checkLive();
772        if (node == null) {
773            throw new IllegalArgumentException("Illegal null node");
774        }
775        Serializable id = node.getHierFragment().get(Model.HIER_PARENT_KEY);
776        return id == null ? null : getNodeById(id);
777    }
778
779    @Override
780    public String getPath(Node node) {
781        checkLive();
782        String path = node.getPath();
783        if (path == null) {
784            path = context.getPath(node.getHierFragment());
785        }
786        return path;
787    }
788
789    /*
790     * Normalize using NFC to avoid decomposed characters (like 'e' + COMBINING ACUTE ACCENT instead of LATIN SMALL
791     * LETTER E WITH ACUTE). NFKC (normalization using compatibility decomposition) is not used, because compatibility
792     * decomposition turns some characters (LATIN SMALL LIGATURE FFI, TRADE MARK SIGN, FULLWIDTH SOLIDUS) into a series
793     * of characters ('f'+'f'+'i', 'T'+'M', '/') that cannot be re-composed into the original, and therefore loses
794     * information.
795     */
796    protected String normalize(String path) {
797        return Normalizer.normalize(path, Normalizer.Form.NFC);
798    }
799
800    /* Does not apply to properties for now (no use case). */
801    @Override
802    public Node getNodeByPath(String path, Node node) {
803        // TODO optimize this to use a dedicated path-based table
804        checkLive();
805        if (path == null) {
806            throw new IllegalArgumentException("Illegal null path");
807        }
808        path = normalize(path);
809        int i;
810        if (path.startsWith("/")) {
811            node = getRootNode();
812            if (path.equals("/")) {
813                return node;
814            }
815            i = 1;
816        } else {
817            if (node == null) {
818                throw new IllegalArgumentException("Illegal relative path with null node: " + path);
819            }
820            i = 0;
821        }
822        String[] names = path.split("/", -1);
823        for (; i < names.length; i++) {
824            String name = names[i];
825            if (name.length() == 0) {
826                throw new IllegalArgumentException("Illegal path with empty component: " + path);
827            }
828            node = getChildNode(node, name, false);
829            if (node == null) {
830                return null;
831            }
832        }
833        return node;
834    }
835
836    @Override
837    public boolean addMixinType(Node node, String mixin) {
838        if (model.getMixinPropertyInfos(mixin) == null) {
839            throw new IllegalArgumentException("No such mixin: " + mixin);
840        }
841        if (model.getDocumentTypeFacets(node.getPrimaryType()).contains(mixin)) {
842            return false; // already present in type
843        }
844        List<String> list = new ArrayList<>(Arrays.asList(node.getMixinTypes()));
845        if (list.contains(mixin)) {
846            return false; // already present in node
847        }
848        Set<String> otherChildrenNames = getChildrenNames(node.getPrimaryType(), list);
849        list.add(mixin);
850        String[] mixins = list.toArray(new String[list.size()]);
851        node.hierFragment.put(Model.MAIN_MIXIN_TYPES_KEY, mixins);
852        // immediately create child nodes (for complex properties) in order
853        // to avoid concurrency issue later on
854        Map<String, String> childrenTypes = model.getMixinComplexChildren(mixin);
855        for (Entry<String, String> es : childrenTypes.entrySet()) {
856            String childName = es.getKey();
857            String childType = es.getValue();
858            // child may already exist if the schema is part of the primary type or another facet
859            if (otherChildrenNames.contains(childName)) {
860                continue;
861            }
862            addChildNode(node, childName, null, childType, true);
863        }
864        return true;
865    }
866
867    @Override
868    public boolean removeMixinType(Node node, String mixin) {
869        List<String> list = new ArrayList<>(Arrays.asList(node.getMixinTypes()));
870        if (!list.remove(mixin)) {
871            return false; // not present in node
872        }
873        String[] mixins = list.toArray(new String[list.size()]);
874        if (mixins.length == 0) {
875            mixins = null;
876        }
877        node.hierFragment.put(Model.MAIN_MIXIN_TYPES_KEY, mixins);
878        Set<String> otherChildrenNames = getChildrenNames(node.getPrimaryType(), list);
879        Map<String, String> childrenTypes = model.getMixinComplexChildren(mixin);
880        for (String childName : childrenTypes.keySet()) {
881            // child must be kept if the schema is part of primary type or another facet
882            if (otherChildrenNames.contains(childName)) {
883                continue;
884            }
885            Node child = getChildNode(node, childName, true);
886            removePropertyNode(child);
887        }
888        node.clearCache();
889        return true;
890    }
891
892    @Override
893    public ScrollResult scroll(String query, int batchSize, int keepAliveSeconds) {
894        return mapper.scroll(query, batchSize, keepAliveSeconds);
895    }
896
897    @Override
898    public ScrollResult scroll(String scrollId) {
899        return mapper.scroll(scrollId);
900    }
901
902    /**
903     * Gets complex children names defined by the primary type and the list of mixins.
904     */
905    protected Set<String> getChildrenNames(String primaryType, List<String> mixins) {
906        Map<String, String> cc = model.getTypeComplexChildren(primaryType);
907        if (cc == null) {
908            cc = Collections.emptyMap();
909        }
910        Set<String> childrenNames = new HashSet<>(cc.keySet());
911        for (String mixin : mixins) {
912            cc = model.getMixinComplexChildren(mixin);
913            if (cc != null) {
914                childrenNames.addAll(cc.keySet());
915            }
916        }
917        return childrenNames;
918    }
919
920    @Override
921    public Node addChildNode(Node parent, String name, Long pos, String typeName, boolean complexProp) {
922        if (pos == null && !complexProp && parent != null) {
923            pos = context.getNextPos(parent.getId(), complexProp);
924        }
925        return addChildNode(null, parent, name, pos, typeName, complexProp);
926    }
927
928    @Override
929    public Node addChildNode(Serializable id, Node parent, String name, Long pos, String typeName,
930            boolean complexProp) {
931        checkLive();
932        if (name == null) {
933            throw new IllegalArgumentException("Illegal null name");
934        }
935        name = normalize(name);
936        if (name.contains("/") || name.equals(".") || name.equals("..")) {
937            throw new IllegalArgumentException("Illegal name: " + name);
938        }
939        if (!model.isType(typeName)) {
940            throw new IllegalArgumentException("Unknown type: " + typeName);
941        }
942        id = generateNewId(id);
943        Serializable parentId = parent == null ? null : parent.hierFragment.getId();
944        Node node = addNode(id, parentId, name, pos, typeName, complexProp);
945        // immediately create child nodes (for complex properties) in order
946        // to avoid concurrency issue later on
947        Map<String, String> childrenTypes = model.getTypeComplexChildren(typeName);
948        for (Entry<String, String> es : childrenTypes.entrySet()) {
949            String childName = es.getKey();
950            String childType = es.getValue();
951            addChildNode(node, childName, null, childType, true);
952        }
953        return node;
954    }
955
956    protected Node addNode(Serializable id, Serializable parentId, String name, Long pos, String typeName,
957            boolean complexProp) {
958        requireReadAclsUpdate();
959        // main info
960        Row hierRow = new Row(Model.HIER_TABLE_NAME, id);
961        hierRow.putNew(Model.HIER_PARENT_KEY, parentId);
962        hierRow.putNew(Model.HIER_CHILD_NAME_KEY, name);
963        hierRow.putNew(Model.HIER_CHILD_POS_KEY, pos);
964        hierRow.putNew(Model.MAIN_PRIMARY_TYPE_KEY, typeName);
965        hierRow.putNew(Model.HIER_CHILD_ISPROPERTY_KEY, Boolean.valueOf(complexProp));
966        if (changeTokenEnabled) {
967            hierRow.putNew(Model.MAIN_SYS_CHANGE_TOKEN_KEY, Model.INITIAL_SYS_CHANGE_TOKEN);
968        }
969        SimpleFragment hierFragment = context.createHierarchyFragment(hierRow);
970        FragmentGroup fragmentGroup = new FragmentGroup(hierFragment, new FragmentsMap());
971        return new Node(context, fragmentGroup, context.getPath(hierFragment));
972    }
973
974    @Override
975    public Node addProxy(Serializable targetId, Serializable versionableId, Node parent, String name, Long pos) {
976        if (!repository.getRepositoryDescriptor().getProxiesEnabled()) {
977            throw new NuxeoException("Proxies are disabled by configuration");
978        }
979        Node proxy = addChildNode(parent, name, pos, Model.PROXY_TYPE, false);
980        proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId);
981        proxy.setSimpleProperty(Model.PROXY_VERSIONABLE_PROP, versionableId);
982        if (changeTokenEnabled) {
983            proxy.setSimpleProperty(Model.MAIN_SYS_CHANGE_TOKEN_PROP, Model.INITIAL_SYS_CHANGE_TOKEN);
984            proxy.setSimpleProperty(Model.MAIN_CHANGE_TOKEN_PROP, Model.INITIAL_CHANGE_TOKEN);
985        }
986        SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME);
987        context.createdProxyFragment(proxyFragment);
988        return proxy;
989    }
990
991    @Override
992    public void setProxyTarget(Node proxy, Serializable targetId) {
993        if (!repository.getRepositoryDescriptor().getProxiesEnabled()) {
994            throw new NuxeoException("Proxies are disabled by configuration");
995        }
996        SimpleProperty prop = proxy.getSimpleProperty(Model.PROXY_TARGET_PROP);
997        Serializable oldTargetId = prop.getValue();
998        if (!oldTargetId.equals(targetId)) {
999            SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME);
1000            context.removedProxyTarget(proxyFragment);
1001            proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId);
1002            context.addedProxyTarget(proxyFragment);
1003        }
1004    }
1005
1006    @Override
1007    public boolean hasChildNode(Node parent, String name, boolean complexProp) {
1008        checkLive();
1009        // TODO could optimize further by not fetching the fragment at all
1010        SimpleFragment fragment = context.getChildHierByName(parent.getId(), normalize(name), complexProp);
1011        return fragment != null;
1012    }
1013
1014    @Override
1015    public Node getChildNode(Node parent, String name, boolean complexProp) {
1016        checkLive();
1017        if (name == null || name.contains("/") || name.equals(".") || name.equals("..")) {
1018            throw new IllegalArgumentException("Illegal name: " + name);
1019        }
1020        SimpleFragment fragment = context.getChildHierByName(parent.getId(), name, complexProp);
1021        return fragment == null ? null : getNodeById(fragment.getId());
1022    }
1023
1024    // TODO optimize with dedicated backend call
1025    @Override
1026    public boolean hasChildren(Node parent, boolean complexProp) {
1027        checkLive();
1028        List<SimpleFragment> children = context.getChildren(parent.getId(), null, complexProp);
1029        if (complexProp) {
1030            return !children.isEmpty();
1031        }
1032        if (children.isEmpty()) {
1033            return false;
1034        }
1035        // we have to check that type names are not obsolete, as they wouldn't be returned
1036        // by getChildren and we must be consistent
1037        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
1038        for (SimpleFragment simpleFragment : children) {
1039            String primaryType = simpleFragment.getString(Model.MAIN_PRIMARY_TYPE_KEY);
1040            if (primaryType.equals(Model.PROXY_TYPE)) {
1041                Node node = getNodeById(simpleFragment.getId(), false);
1042                Serializable targetId = node.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue();
1043                if (targetId == null) {
1044                    // missing target, should not happen, ignore
1045                    continue;
1046                }
1047                Node target = getNodeById(targetId, false);
1048                if (target == null) {
1049                    continue;
1050                }
1051                primaryType = target.getPrimaryType();
1052            }
1053            DocumentType type = schemaManager.getDocumentType(primaryType);
1054            if (type == null) {
1055                // obsolete type, ignored in getChildren
1056                continue;
1057            }
1058            return true;
1059        }
1060        return false;
1061    }
1062
1063    @Override
1064    public List<Node> getChildren(Node parent, String name, boolean complexProp) {
1065        checkLive();
1066        List<SimpleFragment> fragments = context.getChildren(parent.getId(), name, complexProp);
1067        List<Node> nodes = new ArrayList<>(fragments.size());
1068        for (SimpleFragment fragment : fragments) {
1069            Node node = getNodeById(fragment.getId());
1070            if (node == null) {
1071                // cannot happen
1072                log.error("Child node cannot be created: " + fragment.getId());
1073                continue;
1074            }
1075            nodes.add(node);
1076        }
1077        return nodes;
1078    }
1079
1080    @Override
1081    public void orderBefore(Node parent, Node source, Node dest) {
1082        checkLive();
1083        context.orderBefore(parent.getId(), source.getId(), dest == null ? null : dest.getId());
1084    }
1085
1086    @Override
1087    public Node move(Node source, Node parent, String name) {
1088        checkLive();
1089        if (!parent.getId().equals(source.getParentId())) {
1090            flush(); // needed when doing many moves for circular stuff
1091        }
1092        context.move(source, parent.getId(), name);
1093        requireReadAclsUpdate();
1094        return source;
1095    }
1096
1097    @Override
1098    public Node copy(Node source, Node parent, String name) {
1099        checkLive();
1100        flush();
1101        Serializable id = context.copy(source, parent.getId(), name);
1102        requireReadAclsUpdate();
1103        return getNodeById(id);
1104    }
1105
1106    @Override
1107    public void removeNode(Node node) {
1108        checkLive();
1109        flush();
1110        // remove the lock using the lock manager
1111        // TODO children locks?
1112        Serializable id = node.getId();
1113        getLockManager().removeLock(model.idToString(id), null);
1114        // find all descendants
1115        List<NodeInfo> nodeInfos = context.getNodeAndDescendantsInfo(node.getHierFragment());
1116
1117        if (repository.getRepositoryDescriptor().getProxiesEnabled()) {
1118            // if a proxy target is removed, check that all proxies to it are removed
1119            Set<Serializable> removedIds = nodeInfos.stream().map(info -> info.id).collect(Collectors.toSet());
1120            // find proxies pointing to any removed document
1121            Set<Serializable> proxyIds = context.getTargetProxies(removedIds);
1122            for (Serializable proxyId : proxyIds) {
1123                if (!removedIds.contains(proxyId)) {
1124                    Node proxy = getNodeById(proxyId);
1125                    Serializable targetId = (Serializable) proxy.getSingle(Model.PROXY_TARGET_PROP);
1126                    throw new DocumentExistsException(
1127                            "Cannot remove " + id + ", subdocument " + targetId + " is the target of proxy " + proxyId);
1128                }
1129            }
1130        }
1131
1132        // remove all nodes
1133        context.removeNode(node.getHierFragment(), nodeInfos);
1134    }
1135
1136    @Override
1137    public void removePropertyNode(Node node) {
1138        checkLive();
1139        // no flush needed
1140        context.removePropertyNode(node.getHierFragment());
1141    }
1142
1143    @Override
1144    public Node checkIn(Node node, String label, String checkinComment) {
1145        checkLive();
1146        flush();
1147        Serializable id = context.checkIn(node, label, checkinComment);
1148        requireReadAclsUpdate();
1149        // save to reflect changes immediately in database
1150        flush();
1151        return getNodeById(id);
1152    }
1153
1154    @Override
1155    public void checkOut(Node node) {
1156        checkLive();
1157        context.checkOut(node);
1158        requireReadAclsUpdate();
1159    }
1160
1161    @Override
1162    public void restore(Node node, Node version) {
1163        checkLive();
1164        // save done inside method
1165        context.restoreVersion(node, version);
1166        requireReadAclsUpdate();
1167    }
1168
1169    @Override
1170    public Node getVersionByLabel(Serializable versionSeriesId, String label) {
1171        if (label == null) {
1172            return null;
1173        }
1174        List<Node> versions = getVersions(versionSeriesId);
1175        for (Node node : versions) {
1176            String l = (String) node.getSimpleProperty(Model.VERSION_LABEL_PROP).getValue();
1177            if (label.equals(l)) {
1178                return node;
1179            }
1180        }
1181        return null;
1182    }
1183
1184    @Override
1185    public Node getLastVersion(Serializable versionSeriesId) {
1186        checkLive();
1187        List<Serializable> ids = context.getVersionIds(versionSeriesId);
1188        return ids.isEmpty() ? null : getNodeById(ids.get(ids.size() - 1));
1189    }
1190
1191    @Override
1192    public List<Node> getVersions(Serializable versionSeriesId) {
1193        checkLive();
1194        List<Serializable> ids = context.getVersionIds(versionSeriesId);
1195        List<Node> nodes = new ArrayList<>(ids.size());
1196        for (Serializable id : ids) {
1197            nodes.add(getNodeById(id));
1198        }
1199        return nodes;
1200    }
1201
1202    @Override
1203    public List<Node> getProxies(Node document, Node parent) {
1204        checkLive();
1205        if (!repository.getRepositoryDescriptor().getProxiesEnabled()) {
1206            return Collections.emptyList();
1207        }
1208
1209        List<Serializable> ids;
1210        if (document.isVersion()) {
1211            ids = context.getTargetProxyIds(document.getId());
1212        } else {
1213            Serializable versionSeriesId;
1214            if (document.isProxy()) {
1215                versionSeriesId = document.getSimpleProperty(Model.PROXY_VERSIONABLE_PROP).getValue();
1216            } else {
1217                versionSeriesId = document.getId();
1218            }
1219            ids = context.getSeriesProxyIds(versionSeriesId);
1220        }
1221
1222        List<Node> nodes = new LinkedList<>();
1223        for (Serializable id : ids) {
1224            Node node = getNodeById(id);
1225            if (node != null || Boolean.TRUE.booleanValue()) { // XXX
1226                // null if deleted, which means selection wasn't correctly
1227                // updated
1228                nodes.add(node);
1229            }
1230        }
1231
1232        if (parent != null) {
1233            // filter by parent
1234            Serializable parentId = parent.getId();
1235            for (Iterator<Node> it = nodes.iterator(); it.hasNext();) {
1236                Node node = it.next();
1237                if (!parentId.equals(node.getParentId())) {
1238                    it.remove();
1239                }
1240            }
1241        }
1242
1243        return nodes;
1244    }
1245
1246    /**
1247     * Fetches the hierarchy fragment for the given rows and all their ancestors.
1248     *
1249     * @param ids the fragment ids
1250     */
1251    protected List<Fragment> getHierarchyAndAncestors(Collection<Serializable> ids) {
1252        Set<Serializable> allIds = mapper.getAncestorsIds(ids);
1253        allIds.addAll(ids);
1254        List<RowId> rowIds = new ArrayList<>(allIds.size());
1255        for (Serializable id : allIds) {
1256            rowIds.add(new RowId(Model.HIER_TABLE_NAME, id));
1257        }
1258        return context.getMulti(rowIds, true);
1259    }
1260
1261    @Override
1262    public PartialList<Serializable> query(String query, QueryFilter queryFilter, boolean countTotal) {
1263        final Timer.Context timerContext = queryTimer.time();
1264        try {
1265            return mapper.query(query, NXQL.NXQL, queryFilter, countTotal);
1266        } finally {
1267            timerContext.stop();
1268        }
1269    }
1270
1271    @Override
1272    public PartialList<Serializable> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) {
1273        final Timer.Context timerContext = queryTimer.time();
1274        try {
1275            return mapper.query(query, queryType, queryFilter, countUpTo);
1276        } finally {
1277            long duration = timerContext.stop();
1278            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
1279                String msg = String.format("duration_ms:\t%.2f\t%s %s\tquery\t%s", duration / 1000000.0, queryFilter,
1280                        countUpToAsString(countUpTo), query);
1281                if (log.isTraceEnabled()) {
1282                    log.info(msg, new Throwable("Slow query stack trace"));
1283                } else {
1284                    log.info(msg);
1285                }
1286            }
1287        }
1288    }
1289
1290    private String countUpToAsString(long countUpTo) {
1291        if (countUpTo > 0) {
1292            return String.format("count total results up to %d", countUpTo);
1293        }
1294        return countUpTo == -1 ? "count total results UNLIMITED" : "";
1295    }
1296
1297    @Override
1298    public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter,
1299            Object... params) {
1300        return queryAndFetch(query, queryType, queryFilter, false, params);
1301    }
1302
1303    @Override
1304    public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter,
1305            boolean distinctDocuments, Object... params) {
1306        final Timer.Context timerContext = queryTimer.time();
1307        try {
1308            return mapper.queryAndFetch(query, queryType, queryFilter, distinctDocuments, params);
1309        } finally {
1310            long duration = timerContext.stop();
1311            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
1312                String msg = String.format("duration_ms:\t%.2f\t%s\tqueryAndFetch\t%s", duration / 1000000.0,
1313                        queryFilter, query);
1314                if (log.isTraceEnabled()) {
1315                    log.info(msg, new Throwable("Slow query stack trace"));
1316                } else {
1317                    log.info(msg);
1318                }
1319            }
1320        }
1321    }
1322
1323    @Override
1324    public PartialList<Map<String, Serializable>> queryProjection(String query, String queryType,
1325            QueryFilter queryFilter, boolean distinctDocuments, long countUpTo, Object... params) {
1326        final Timer.Context timerContext = queryTimer.time();
1327        try {
1328            return mapper.queryProjection(query, queryType, queryFilter, distinctDocuments, countUpTo, params);
1329        } finally {
1330            long duration = timerContext.stop();
1331            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
1332                String msg = String.format("duration_ms:\t%.2f\t%s\tqueryProjection\t%s", duration / 1000000.0,
1333                        queryFilter, query);
1334                if (log.isTraceEnabled()) {
1335                    log.info(msg, new Throwable("Slow query stack trace"));
1336                } else {
1337                    log.info(msg);
1338                }
1339            }
1340        }
1341    }
1342
1343    @Override
1344    public LockManager getLockManager() {
1345        return repository.getLockManager();
1346    }
1347
1348    @Override
1349    public void requireReadAclsUpdate() {
1350        readAclsChanged = true;
1351    }
1352
1353    @Override
1354    public void updateReadAcls() {
1355        final Timer.Context timerContext = aclrUpdateTimer.time();
1356        try {
1357            mapper.updateReadAcls();
1358            readAclsChanged = false;
1359        } finally {
1360            timerContext.stop();
1361        }
1362    }
1363
1364    @Override
1365    public void rebuildReadAcls() {
1366        mapper.rebuildReadAcls();
1367        readAclsChanged = false;
1368    }
1369
1370    private void computeRootNode() {
1371        String repositoryId = repository.getName();
1372        Serializable rootId = mapper.getRootId(repositoryId);
1373        if (rootId == null && COMPAT_REPOSITORY_NAME) {
1374            // compat, old repositories had fixed id "default"
1375            rootId = mapper.getRootId("default");
1376        }
1377        if (rootId == null) {
1378            log.debug("Creating root");
1379            rootNode = addRootNode();
1380            addRootACP();
1381            save();
1382            // record information about the root id
1383            mapper.setRootId(repositoryId, rootNode.getId());
1384        } else {
1385            rootNode = getNodeById(rootId, false);
1386        }
1387    }
1388
1389    // TODO factor with addChildNode
1390    private Node addRootNode() {
1391        Serializable id = generateNewId(null);
1392        return addNode(id, null, "", null, Model.ROOT_TYPE, false);
1393    }
1394
1395    private void addRootACP() {
1396        ACLRow[] aclrows = new ACLRow[3];
1397        // TODO put groups in their proper place. like that now for consistency.
1398        aclrows[0] = new ACLRow(0, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATORS,
1399                null);
1400        aclrows[1] = new ACLRow(1, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATOR,
1401                null);
1402        aclrows[2] = new ACLRow(2, ACL.LOCAL_ACL, true, SecurityConstants.READ, SecurityConstants.MEMBERS, null);
1403        rootNode.setCollectionProperty(Model.ACL_PROP, aclrows);
1404        requireReadAclsUpdate();
1405    }
1406
1407    // public Node newNodeInstance() needed ?
1408
1409    public void checkPermission(String absPath, String actions) {
1410        checkLive();
1411        // TODO Auto-generated method stub
1412        throw new RuntimeException("Not implemented");
1413    }
1414
1415    public boolean hasPendingChanges() {
1416        checkLive();
1417        // TODO Auto-generated method stub
1418        throw new RuntimeException("Not implemented");
1419    }
1420
1421    public void markReferencedBinaries() {
1422        checkLive();
1423        mapper.markReferencedBinaries();
1424    }
1425
1426    public int cleanupDeletedDocuments(int max, Calendar beforeTime) {
1427        checkLive();
1428        if (!repository.getRepositoryDescriptor().getSoftDeleteEnabled()) {
1429            return 0;
1430        }
1431        return mapper.cleanupDeletedRows(max, beforeTime);
1432    }
1433
1434    /*
1435     * ----- XAResource -----
1436     */
1437
1438    @Override
1439    public boolean isSameRM(XAResource xaresource) {
1440        return xaresource == this;
1441    }
1442
1443    @Override
1444    public void start(Xid xid, int flags) throws XAException {
1445        if (flags == TMNOFLAGS) {
1446            try {
1447                processReceivedInvalidations();
1448            } catch (NuxeoException e) {
1449                log.error("Could not start transaction", e);
1450                throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e);
1451            }
1452        }
1453        mapper.start(xid, flags);
1454        inTransaction = true;
1455        checkThreadStart();
1456    }
1457
1458    @Override
1459    public void end(Xid xid, int flags) throws XAException {
1460        boolean failed = true;
1461        try {
1462            if (flags != TMFAIL) {
1463                try {
1464                    flush();
1465                } catch (ConcurrentUpdateException e) {
1466                    TransactionHelper.noteSuppressedException(e);
1467                    log.debug("Exception during transaction commit", e);
1468                    // set rollback only manually instead of throwing, this avoids
1469                    // a spurious log in Geronimo TransactionImpl and has the same effect
1470                    TransactionHelper.setTransactionRollbackOnly();
1471                    return;
1472                } catch (NuxeoException e) {
1473                    log.error("Exception during transaction commit", e);
1474                    throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e);
1475                }
1476            }
1477            failed = false;
1478            mapper.end(xid, flags);
1479        } finally {
1480            if (failed) {
1481                mapper.end(xid, TMFAIL);
1482                // rollback done by tx manager
1483            }
1484        }
1485    }
1486
1487    @Override
1488    public int prepare(Xid xid) throws XAException {
1489        int res = mapper.prepare(xid);
1490        if (res == XA_RDONLY) {
1491            // Read-only optimization, commit() won't be called by the TM.
1492            // It's important to nevertheless send invalidations because
1493            // Oracle, in tightly-coupled transaction mode, can return
1494            // this status even when some changes were actually made
1495            // (they just will be committed by another resource).
1496            // See NXP-7943
1497            commitDone();
1498        }
1499        return res;
1500    }
1501
1502    @Override
1503    public void commit(Xid xid, boolean onePhase) throws XAException {
1504        try {
1505            mapper.commit(xid, onePhase);
1506        } finally {
1507            commitDone();
1508        }
1509    }
1510
1511    protected void commitDone() throws XAException {
1512        inTransaction = false;
1513        try {
1514            try {
1515                sendInvalidationsToOthers();
1516            } finally {
1517                checkThreadEnd();
1518            }
1519        } catch (NuxeoException e) {
1520            log.error("Could not send invalidations", e);
1521            throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e);
1522        }
1523    }
1524
1525    @Override
1526    public void rollback(Xid xid) throws XAException {
1527        try {
1528            try {
1529                mapper.rollback(xid);
1530            } finally {
1531                rollback();
1532            }
1533        } finally {
1534            inTransaction = false;
1535            // no invalidations to send
1536            checkThreadEnd();
1537        }
1538    }
1539
1540    @Override
1541    public void forget(Xid xid) throws XAException {
1542        mapper.forget(xid);
1543    }
1544
1545    @Override
1546    public Xid[] recover(int flag) throws XAException {
1547        return mapper.recover(flag);
1548    }
1549
1550    @Override
1551    public boolean setTransactionTimeout(int seconds) throws XAException {
1552        return mapper.setTransactionTimeout(seconds);
1553    }
1554
1555    @Override
1556    public int getTransactionTimeout() throws XAException {
1557        return mapper.getTransactionTimeout();
1558    }
1559
1560    public long getCacheSize() {
1561        return context.getCacheSize();
1562    }
1563
1564    public long getCacheMapperSize() {
1565        return context.getCacheMapperSize();
1566    }
1567
1568    public long getCachePristineSize() {
1569        return context.getCachePristineSize();
1570    }
1571
1572    public long getCacheSelectionSize() {
1573        return context.getCacheSelectionSize();
1574    }
1575
1576    @Override
1577    public Map<String, String> getBinaryFulltext(Serializable id) {
1578        if (repository.getRepositoryDescriptor().getFulltextDescriptor().getFulltextDisabled()) {
1579            return null;
1580        }
1581        RowId rowId = new RowId(Model.FULLTEXT_TABLE_NAME, id);
1582        return mapper.getBinaryFulltext(rowId);
1583    }
1584
1585    @Override
1586    public boolean isChangeTokenEnabled() {
1587        return changeTokenEnabled;
1588    }
1589
1590    @Override
1591    public void markUserChange(Serializable id) {
1592        context.markUserChange(id);
1593    }
1594
1595}