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