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