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