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.repository.RepositoryManager;
054import org.nuxeo.ecm.core.api.security.ACL;
055import org.nuxeo.ecm.core.api.security.SecurityConstants;
056import org.nuxeo.ecm.core.model.LockManager;
057import org.nuxeo.ecm.core.query.QueryFilter;
058import org.nuxeo.ecm.core.query.sql.NXQL;
059import org.nuxeo.ecm.core.schema.DocumentType;
060import org.nuxeo.ecm.core.schema.SchemaManager;
061import org.nuxeo.ecm.core.storage.FulltextParser;
062import org.nuxeo.ecm.core.storage.FulltextUpdaterWork;
063import org.nuxeo.ecm.core.storage.FulltextUpdaterWork.IndexAndText;
064import org.nuxeo.ecm.core.storage.sql.PersistenceContext.PathAndId;
065import org.nuxeo.ecm.core.storage.sql.RowMapper.RowBatch;
066import org.nuxeo.ecm.core.storage.sql.coremodel.SQLFulltextExtractorWork;
067import org.nuxeo.ecm.core.work.api.Work;
068import org.nuxeo.ecm.core.work.api.WorkManager;
069import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling;
070import org.nuxeo.runtime.api.Framework;
071import org.nuxeo.runtime.metrics.MetricsService;
072import org.nuxeo.runtime.transaction.TransactionHelper;
073
074import com.codahale.metrics.MetricRegistry;
075import com.codahale.metrics.SharedMetricRegistries;
076import com.codahale.metrics.Timer;
077
078/**
079 * The session is the main high level access point to data from the underlying database.
080 */
081public class SessionImpl implements Session, XAResource {
082
083    private static final Log log = LogFactory.getLog(SessionImpl.class);
084
085    /**
086     * Set this system property to false if you don't want repositories to be looked up under the compatibility name
087     * "default" in the "repositories" table.
088     * <p>
089     * Only do this if you start from an empty database, or if you have migrated the "repositories" table by hand, or if
090     * you need to create a new repository in a database already containing a "default" repository (table sharing, not
091     * recommended).
092     */
093    public static final String COMPAT_REPOSITORY_NAME_KEY = "org.nuxeo.vcs.repository.name.default.compat";
094
095    private static final boolean COMPAT_REPOSITORY_NAME = Boolean.parseBoolean(Framework.getProperty(
096            COMPAT_REPOSITORY_NAME_KEY, "true"));
097
098    protected final RepositoryImpl repository;
099
100    private final Mapper mapper;
101
102    private final Model model;
103
104    protected final FulltextParser fulltextParser;
105
106    // public because used by unit tests
107    public final PersistenceContext context;
108
109    private volatile boolean live;
110
111    private boolean inTransaction;
112
113    private Node rootNode;
114
115    private long threadId;
116
117    private String threadName;
118
119    private Throwable threadStack;
120
121    private boolean readAclsChanged;
122
123    // @since 5.7
124    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
125
126    private final Timer saveTimer;
127
128    private final Timer queryTimer;
129
130    private final Timer aclrUpdateTimer;
131
132    private static final java.lang.String LOG_MIN_DURATION_KEY = "org.nuxeo.vcs.query.log_min_duration_ms";
133
134    private static final long LOG_MIN_DURATION_NS = Long.parseLong(Framework.getProperty(LOG_MIN_DURATION_KEY, "-1")) * 1000000;
135
136    public SessionImpl(RepositoryImpl repository, Model model, Mapper mapper) {
137        this.repository = repository;
138        this.mapper = mapper;
139        this.model = model;
140        context = new PersistenceContext(model, mapper, this);
141        live = true;
142        readAclsChanged = false;
143
144        try {
145            fulltextParser = repository.fulltextParserClass.newInstance();
146        } catch (ReflectiveOperationException e) {
147            throw new NuxeoException(e);
148        }
149        saveTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), "saves"));
150        queryTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(), "queries"));
151        aclrUpdateTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories", repository.getName(),
152                "aclr-updates"));
153
154        computeRootNode();
155    }
156
157    public void checkLive() {
158        if (!live) {
159            throw new IllegalStateException("Session is not live");
160        }
161        checkThread();
162    }
163
164    // called by NetServlet when forwarding remote NetMapper calls.
165    @Override
166    public Mapper getMapper() {
167        return mapper;
168    }
169
170    /**
171     * Gets the XAResource. Called by the ManagedConnectionImpl, which actually wraps it in a connection-aware
172     * implementation.
173     */
174    public XAResource getXAResource() {
175        return this;
176    }
177
178    /**
179     * Clears all the caches. Called by RepositoryManagement.
180     */
181    protected int clearCaches() {
182        if (inTransaction) {
183            // avoid potential multi-threaded access to active session
184            return 0;
185        }
186        checkThreadEnd();
187        return context.clearCaches();
188    }
189
190    protected PersistenceContext getContext() {
191        return context;
192    }
193
194    protected void rollback() {
195        context.clearCaches();
196    }
197
198    protected void checkThread() {
199        if (threadId == 0) {
200            return;
201        }
202        long currentThreadId = Thread.currentThread().getId();
203        if (threadId == currentThreadId) {
204            return;
205        }
206        String currentThreadName = Thread.currentThread().getName();
207        String msg = String.format("Concurrency Error: Session was started in thread %s (%s)"
208                + " but is being used in thread %s (%s)", threadId, threadName, currentThreadId, currentThreadName);
209        throw new IllegalStateException(msg, threadStack);
210    }
211
212    protected void checkThreadStart() {
213        threadId = Thread.currentThread().getId();
214        threadName = Thread.currentThread().getName();
215        if (log.isDebugEnabled()) {
216            threadStack = new Throwable("owner stack trace");
217        }
218    }
219
220    protected void checkThreadEnd() {
221        threadId = 0;
222        threadName = null;
223        threadStack = null;
224    }
225
226    /**
227     * Generates a new id, or used a pre-generated one (import).
228     */
229    protected Serializable generateNewId(Serializable id) {
230        return context.generateNewId(id);
231    }
232
233    protected boolean isIdNew(Serializable id) {
234        return context.isIdNew(id);
235    }
236
237    /*
238     * ----- javax.resource.cci.Connection -----
239     */
240
241    @Override
242    public void close() throws ResourceException {
243        try {
244            checkLive();
245            closeSession();
246            repository.closeSession(this);
247        } catch (Exception cause) {
248            throw new ResourceException(cause);
249        }
250    }
251
252    protected void closeSession() {
253        live = false;
254        context.clearCaches();
255        // close the mapper and therefore the connection
256        mapper.close();
257        // don't clean the caches, we keep the pristine cache around
258        // TODO this is getting destroyed, we can clean everything
259    }
260
261    @Override
262    public Interaction createInteraction() throws ResourceException {
263        throw new UnsupportedOperationException();
264    }
265
266    @Override
267    public LocalTransaction getLocalTransaction() throws ResourceException {
268        throw new UnsupportedOperationException();
269    }
270
271    @Override
272    public ConnectionMetaData getMetaData() throws ResourceException {
273        throw new UnsupportedOperationException();
274    }
275
276    @Override
277    public ResultSetInfo getResultSetInfo() throws ResourceException {
278        throw new UnsupportedOperationException();
279    }
280
281    /*
282     * ----- Session -----
283     */
284
285    @Override
286    public boolean isLive() {
287        return live;
288    }
289
290    @Override
291    public boolean isStateSharedByAllThreadSessions() {
292        // only the JCA handle returns true
293        return false;
294    }
295
296    @Override
297    public String getRepositoryName() {
298        return repository.getName();
299    }
300
301    @Override
302    public Model getModel() {
303        return model;
304    }
305
306    @Override
307    public Node getRootNode() {
308        checkLive();
309        return rootNode;
310    }
311
312    @Override
313    public void save() {
314        final Timer.Context timerContext = saveTimer.time();
315        try {
316            checkLive();
317            flush();
318            if (!inTransaction) {
319                sendInvalidationsToOthers();
320                // as we don't have a way to know when the next
321                // non-transactional
322                // statement will start, process invalidations immediately
323            }
324            processReceivedInvalidations();
325        } finally {
326            timerContext.stop();
327        }
328    }
329
330    protected void flush() {
331        checkThread();
332        List<Work> works;
333        if (!repository.getRepositoryDescriptor().getFulltextDescriptor().getFulltextDisabled()) {
334            works = getFulltextWorks();
335        } else {
336            works = Collections.emptyList();
337        }
338        doFlush();
339        if (readAclsChanged) {
340            updateReadAcls();
341        }
342        scheduleWork(works);
343        checkInvalidationsConflict();
344    }
345
346    protected void scheduleWork(List<Work> works) {
347        // do async fulltext indexing only if high-level sessions are available
348        RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class);
349        if (repositoryManager != null && !works.isEmpty()) {
350            WorkManager workManager = Framework.getLocalService(WorkManager.class);
351            for (Work work : works) {
352                // schedule work post-commit
353                // in non-tx mode, this may execute it nearly immediately
354                workManager.schedule(work, Scheduling.IF_NOT_SCHEDULED, true);
355            }
356        }
357    }
358
359    protected void doFlush() {
360        List<Fragment> fragmentsToClearDirty = new ArrayList<>(0);
361        RowBatch batch = context.getSaveBatch(fragmentsToClearDirty);
362        if (!batch.isEmpty()) {
363            log.debug("Saving session");
364            // execute the batch
365            mapper.write(batch);
366            log.debug("End of save");
367            for (Fragment fragment : fragmentsToClearDirty) {
368                fragment.clearDirty();
369            }
370        }
371    }
372
373    protected Serializable getContainingDocument(Serializable id) {
374        return context.getContainingDocument(id);
375    }
376
377    /**
378     * Gets the fulltext updates to do. Called at save() time.
379     *
380     * @return a list of {@link Work} instances to schedule post-commit.
381     */
382    protected List<Work> getFulltextWorks() {
383        Set<Serializable> dirtyStrings = new HashSet<Serializable>();
384        Set<Serializable> dirtyBinaries = new HashSet<Serializable>();
385        context.findDirtyDocuments(dirtyStrings, dirtyBinaries);
386        if (dirtyStrings.isEmpty() && dirtyBinaries.isEmpty()) {
387            return Collections.emptyList();
388        }
389
390        List<Work> works = new LinkedList<Work>();
391        getFulltextSimpleWorks(works, dirtyStrings);
392        getFulltextBinariesWorks(works, dirtyBinaries);
393        return works;
394    }
395
396    protected void getFulltextSimpleWorks(List<Work> works, Set<Serializable> dirtyStrings) {
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 (!model.getFulltextConfiguration().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 : model.getFulltextConfiguration().indexNames) {
424                Set<String> paths;
425                if (model.getFulltextConfiguration().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 = model.getFulltextConfiguration().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    /**
885     * Gets complex children names defined by the primary type and the list of mixins.
886     */
887    protected Set<String> getChildrenNames(String primaryType, List<String> mixins) {
888        Map<String, String> cc = model.getTypeComplexChildren(primaryType);
889        if (cc == null) {
890            cc = Collections.emptyMap();
891        }
892        Set<String> childrenNames = new HashSet<>(cc.keySet());
893        for (String mixin : mixins) {
894            cc = model.getMixinComplexChildren(mixin);
895            if (cc != null) {
896                childrenNames.addAll(cc.keySet());
897            }
898        }
899        return childrenNames;
900    }
901
902    @Override
903    public Node addChildNode(Node parent, String name, Long pos, String typeName, boolean complexProp) {
904        if (pos == null && !complexProp && parent != null) {
905            pos = context.getNextPos(parent.getId(), complexProp);
906        }
907        return addChildNode(null, parent, name, pos, typeName, complexProp);
908    }
909
910    @Override
911    public Node addChildNode(Serializable id, Node parent, String name, Long pos, String typeName,
912            boolean complexProp) {
913        checkLive();
914        if (name == null) {
915            throw new IllegalArgumentException("Illegal null name");
916        }
917        name = normalize(name);
918        if (name.contains("/") || name.equals(".") || name.equals("..")) {
919            throw new IllegalArgumentException("Illegal name: " + name);
920        }
921        if (!model.isType(typeName)) {
922            throw new IllegalArgumentException("Unknown type: " + typeName);
923        }
924        id = generateNewId(id);
925        Serializable parentId = parent == null ? null : parent.hierFragment.getId();
926        Node node = addNode(id, parentId, name, pos, typeName, complexProp);
927        // immediately create child nodes (for complex properties) in order
928        // to avoid concurrency issue later on
929        Map<String, String> childrenTypes = model.getTypeComplexChildren(typeName);
930        for (Entry<String, String> es : childrenTypes.entrySet()) {
931            String childName = es.getKey();
932            String childType = es.getValue();
933            addChildNode(node, childName, null, childType, true);
934        }
935        return node;
936    }
937
938    protected Node addNode(Serializable id, Serializable parentId, String name, Long pos, String typeName,
939            boolean complexProp) {
940        requireReadAclsUpdate();
941        // main info
942        Row hierRow = new Row(Model.HIER_TABLE_NAME, id);
943        hierRow.putNew(Model.HIER_PARENT_KEY, parentId);
944        hierRow.putNew(Model.HIER_CHILD_NAME_KEY, name);
945        hierRow.putNew(Model.HIER_CHILD_POS_KEY, pos);
946        hierRow.putNew(Model.MAIN_PRIMARY_TYPE_KEY, typeName);
947        hierRow.putNew(Model.HIER_CHILD_ISPROPERTY_KEY, Boolean.valueOf(complexProp));
948        SimpleFragment hierFragment = context.createHierarchyFragment(hierRow);
949        FragmentGroup fragmentGroup = new FragmentGroup(hierFragment, new FragmentsMap());
950        return new Node(context, fragmentGroup, context.getPath(hierFragment));
951    }
952
953    @Override
954    public Node addProxy(Serializable targetId, Serializable versionableId, Node parent, String name, Long pos) {
955        if (!repository.getRepositoryDescriptor().getProxiesEnabled()) {
956            throw new NuxeoException("Proxies are disabled by configuration");
957        }
958        Node proxy = addChildNode(parent, name, pos, Model.PROXY_TYPE, false);
959        proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId);
960        proxy.setSimpleProperty(Model.PROXY_VERSIONABLE_PROP, versionableId);
961        SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME);
962        context.createdProxyFragment(proxyFragment);
963        return proxy;
964    }
965
966    @Override
967    public void setProxyTarget(Node proxy, Serializable targetId) {
968        if (!repository.getRepositoryDescriptor().getProxiesEnabled()) {
969            throw new NuxeoException("Proxies are disabled by configuration");
970        }
971        SimpleProperty prop = proxy.getSimpleProperty(Model.PROXY_TARGET_PROP);
972        Serializable oldTargetId = prop.getValue();
973        if (!oldTargetId.equals(targetId)) {
974            SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME);
975            context.removedProxyTarget(proxyFragment);
976            proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId);
977            context.addedProxyTarget(proxyFragment);
978        }
979    }
980
981    @Override
982    public boolean hasChildNode(Node parent, String name, boolean complexProp) {
983        checkLive();
984        // TODO could optimize further by not fetching the fragment at all
985        SimpleFragment fragment = context.getChildHierByName(parent.getId(), normalize(name), complexProp);
986        return fragment != null;
987    }
988
989    @Override
990    public Node getChildNode(Node parent, String name, boolean complexProp) {
991        checkLive();
992        if (name == null || name.contains("/") || name.equals(".") || name.equals("..")) {
993            throw new IllegalArgumentException("Illegal name: " + name);
994        }
995        SimpleFragment fragment = context.getChildHierByName(parent.getId(), name, complexProp);
996        return fragment == null ? null : getNodeById(fragment.getId());
997    }
998
999    // TODO optimize with dedicated backend call
1000    @Override
1001    public boolean hasChildren(Node parent, boolean complexProp) {
1002        checkLive();
1003        List<SimpleFragment> children = context.getChildren(parent.getId(), null, complexProp);
1004        if (complexProp) {
1005            return !children.isEmpty();
1006        }
1007        if (children.isEmpty()) {
1008            return false;
1009        }
1010        // we have to check that type names are not obsolete, as they wouldn't be returned
1011        // by getChildren and we must be consistent
1012        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
1013        for (SimpleFragment simpleFragment : children) {
1014            String primaryType = simpleFragment.getString(Model.MAIN_PRIMARY_TYPE_KEY);
1015            if (primaryType.equals(Model.PROXY_TYPE)) {
1016                Node node = getNodeById(simpleFragment.getId(), false);
1017                Serializable targetId = node.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue();
1018                if (targetId == null) {
1019                    // missing target, should not happen, ignore
1020                    continue;
1021                }
1022                Node target = getNodeById(targetId, false);
1023                if (target == null) {
1024                    continue;
1025                }
1026                primaryType = target.getPrimaryType();
1027            }
1028            DocumentType type = schemaManager.getDocumentType(primaryType);
1029            if (type == null) {
1030                // obsolete type, ignored in getChildren
1031                continue;
1032            }
1033            return true;
1034        }
1035        return false;
1036    }
1037
1038    @Override
1039    public List<Node> getChildren(Node parent, String name, boolean complexProp) {
1040        checkLive();
1041        List<SimpleFragment> fragments = context.getChildren(parent.getId(), name, complexProp);
1042        List<Node> nodes = new ArrayList<Node>(fragments.size());
1043        for (SimpleFragment fragment : fragments) {
1044            Node node = getNodeById(fragment.getId());
1045            if (node == null) {
1046                // cannot happen
1047                log.error("Child node cannot be created: " + fragment.getId());
1048                continue;
1049            }
1050            nodes.add(node);
1051        }
1052        return nodes;
1053    }
1054
1055    @Override
1056    public void orderBefore(Node parent, Node source, Node dest) {
1057        checkLive();
1058        context.orderBefore(parent.getId(), source.getId(), dest == null ? null : dest.getId());
1059    }
1060
1061    @Override
1062    public Node move(Node source, Node parent, String name) {
1063        checkLive();
1064        if (!parent.getId().equals(source.getParentId())) {
1065            flush(); // needed when doing many moves for circular stuff
1066        }
1067        context.move(source, parent.getId(), name);
1068        requireReadAclsUpdate();
1069        return source;
1070    }
1071
1072    @Override
1073    public Node copy(Node source, Node parent, String name) {
1074        checkLive();
1075        flush();
1076        Serializable id = context.copy(source, parent.getId(), name);
1077        requireReadAclsUpdate();
1078        return getNodeById(id);
1079    }
1080
1081    @Override
1082    public void removeNode(Node node) {
1083        checkLive();
1084        flush();
1085        // remove the lock using the lock manager
1086        // TODO children locks?
1087        getLockManager().removeLock(model.idToString(node.getId()), null);
1088        context.removeNode(node.getHierFragment());
1089    }
1090
1091    @Override
1092    public void removePropertyNode(Node node) {
1093        checkLive();
1094        // no flush needed
1095        context.removePropertyNode(node.getHierFragment());
1096    }
1097
1098    @Override
1099    public Node checkIn(Node node, String label, String checkinComment) {
1100        checkLive();
1101        flush();
1102        Serializable id = context.checkIn(node, label, checkinComment);
1103        requireReadAclsUpdate();
1104        // save to reflect changes immediately in database
1105        flush();
1106        return getNodeById(id);
1107    }
1108
1109    @Override
1110    public void checkOut(Node node) {
1111        checkLive();
1112        context.checkOut(node);
1113        requireReadAclsUpdate();
1114    }
1115
1116    @Override
1117    public void restore(Node node, Node version) {
1118        checkLive();
1119        // save done inside method
1120        context.restoreVersion(node, version);
1121        requireReadAclsUpdate();
1122    }
1123
1124    @Override
1125    public Node getVersionByLabel(Serializable versionSeriesId, String label) {
1126        if (label == null) {
1127            return null;
1128        }
1129        List<Node> versions = getVersions(versionSeriesId);
1130        for (Node node : versions) {
1131            String l = (String) node.getSimpleProperty(Model.VERSION_LABEL_PROP).getValue();
1132            if (label.equals(l)) {
1133                return node;
1134            }
1135        }
1136        return null;
1137    }
1138
1139    @Override
1140    public Node getLastVersion(Serializable versionSeriesId) {
1141        checkLive();
1142        List<Serializable> ids = context.getVersionIds(versionSeriesId);
1143        return ids.isEmpty() ? null : getNodeById(ids.get(ids.size() - 1));
1144    }
1145
1146    @Override
1147    public List<Node> getVersions(Serializable versionSeriesId) {
1148        checkLive();
1149        List<Serializable> ids = context.getVersionIds(versionSeriesId);
1150        List<Node> nodes = new ArrayList<Node>(ids.size());
1151        for (Serializable id : ids) {
1152            nodes.add(getNodeById(id));
1153        }
1154        return nodes;
1155    }
1156
1157    @Override
1158    public List<Node> getProxies(Node document, Node parent) {
1159        checkLive();
1160        if (!repository.getRepositoryDescriptor().getProxiesEnabled()) {
1161            return Collections.emptyList();
1162        }
1163
1164        List<Serializable> ids;
1165        if (document.isVersion()) {
1166            ids = context.getTargetProxyIds(document.getId());
1167        } else {
1168            Serializable versionSeriesId;
1169            if (document.isProxy()) {
1170                versionSeriesId = document.getSimpleProperty(Model.PROXY_VERSIONABLE_PROP).getValue();
1171            } else {
1172                versionSeriesId = document.getId();
1173            }
1174            ids = context.getSeriesProxyIds(versionSeriesId);
1175        }
1176
1177        List<Node> nodes = new LinkedList<Node>();
1178        for (Serializable id : ids) {
1179            Node node = getNodeById(id);
1180            if (node != null || Boolean.TRUE.booleanValue()) { // XXX
1181                // null if deleted, which means selection wasn't correctly
1182                // updated
1183                nodes.add(node);
1184            }
1185        }
1186
1187        if (parent != null) {
1188            // filter by parent
1189            Serializable parentId = parent.getId();
1190            for (Iterator<Node> it = nodes.iterator(); it.hasNext();) {
1191                Node node = it.next();
1192                if (!parentId.equals(node.getParentId())) {
1193                    it.remove();
1194                }
1195            }
1196        }
1197
1198        return nodes;
1199    }
1200
1201    /**
1202     * Fetches the hierarchy fragment for the given rows and all their ancestors.
1203     *
1204     * @param ids the fragment ids
1205     */
1206    protected List<Fragment> getHierarchyAndAncestors(Collection<Serializable> ids) {
1207        Set<Serializable> allIds = mapper.getAncestorsIds(ids);
1208        allIds.addAll(ids);
1209        List<RowId> rowIds = new ArrayList<RowId>(allIds.size());
1210        for (Serializable id : allIds) {
1211            rowIds.add(new RowId(Model.HIER_TABLE_NAME, id));
1212        }
1213        return context.getMulti(rowIds, true);
1214    }
1215
1216    @Override
1217    public PartialList<Serializable> query(String query, QueryFilter queryFilter, boolean countTotal) {
1218        final Timer.Context timerContext = queryTimer.time();
1219        try {
1220            return mapper.query(query, NXQL.NXQL, queryFilter, countTotal);
1221        } finally {
1222            timerContext.stop();
1223        }
1224    }
1225
1226    @Override
1227    public PartialList<Serializable> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) {
1228        final Timer.Context timerContext = queryTimer.time();
1229        try {
1230            return mapper.query(query, queryType, queryFilter, countUpTo);
1231        } finally {
1232            long duration = timerContext.stop();
1233            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
1234                String msg = String.format("duration_ms:\t%.2f\t%s %s\tquery\t%s", duration / 1000000.0, queryFilter,
1235                        countUpToAsString(countUpTo), query);
1236                if (log.isTraceEnabled()) {
1237                    log.info(msg, new Throwable("Slow query stack trace"));
1238                } else {
1239                    log.info(msg);
1240                }
1241            }
1242        }
1243    }
1244
1245    private String countUpToAsString(long countUpTo) {
1246        if (countUpTo > 0) {
1247            return String.format("count total results up to %d", countUpTo);
1248        }
1249        return countUpTo == -1 ? "count total results UNLIMITED" : "";
1250    }
1251
1252    @Override
1253    public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter,
1254            Object... params) {
1255        return queryAndFetch(query, queryType, queryFilter, false, params);
1256    }
1257
1258    @Override
1259    public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter,
1260            boolean distinctDocuments, Object... params) {
1261        final Timer.Context timerContext = queryTimer.time();
1262        try {
1263            return mapper.queryAndFetch(query, queryType, queryFilter, distinctDocuments, params);
1264        } finally {
1265            long duration = timerContext.stop();
1266            if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) {
1267                String msg = String.format("duration_ms:\t%.2f\t%s\tqueryAndFetch\t%s", duration / 1000000.0,
1268                        queryFilter, query);
1269                if (log.isTraceEnabled()) {
1270                    log.info(msg, new Throwable("Slow query stack trace"));
1271                } else {
1272                    log.info(msg);
1273                }
1274            }
1275        }
1276    }
1277
1278    @Override
1279    public LockManager getLockManager() {
1280        return repository.getLockManager();
1281    }
1282
1283    @Override
1284    public void requireReadAclsUpdate() {
1285        readAclsChanged = true;
1286    }
1287
1288    @Override
1289    public void updateReadAcls() {
1290        final Timer.Context timerContext = aclrUpdateTimer.time();
1291        try {
1292            mapper.updateReadAcls();
1293            readAclsChanged = false;
1294        } finally {
1295            timerContext.stop();
1296        }
1297    }
1298
1299    @Override
1300    public void rebuildReadAcls() {
1301        mapper.rebuildReadAcls();
1302        readAclsChanged = false;
1303    }
1304
1305    private void computeRootNode() {
1306        String repositoryId = repository.getName();
1307        Serializable rootId = mapper.getRootId(repositoryId);
1308        if (rootId == null && COMPAT_REPOSITORY_NAME) {
1309            // compat, old repositories had fixed id "default"
1310            rootId = mapper.getRootId("default");
1311        }
1312        if (rootId == null) {
1313            log.debug("Creating root");
1314            rootNode = addRootNode();
1315            addRootACP();
1316            save();
1317            // record information about the root id
1318            mapper.setRootId(repositoryId, rootNode.getId());
1319        } else {
1320            rootNode = getNodeById(rootId, false);
1321        }
1322    }
1323
1324    // TODO factor with addChildNode
1325    private Node addRootNode() {
1326        Serializable id = generateNewId(null);
1327        return addNode(id, null, "", null, Model.ROOT_TYPE, false);
1328    }
1329
1330    private void addRootACP() {
1331        ACLRow[] aclrows = new ACLRow[3];
1332        // TODO put groups in their proper place. like that now for consistency.
1333        aclrows[0] = new ACLRow(0, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATORS,
1334                null);
1335        aclrows[1] = new ACLRow(1, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATOR,
1336                null);
1337        aclrows[2] = new ACLRow(2, ACL.LOCAL_ACL, true, SecurityConstants.READ, SecurityConstants.MEMBERS, null);
1338        rootNode.setCollectionProperty(Model.ACL_PROP, aclrows);
1339        requireReadAclsUpdate();
1340    }
1341
1342    // public Node newNodeInstance() needed ?
1343
1344    public void checkPermission(String absPath, String actions) {
1345        checkLive();
1346        // TODO Auto-generated method stub
1347        throw new RuntimeException("Not implemented");
1348    }
1349
1350    public boolean hasPendingChanges() {
1351        checkLive();
1352        // TODO Auto-generated method stub
1353        throw new RuntimeException("Not implemented");
1354    }
1355
1356    public void markReferencedBinaries() {
1357        checkLive();
1358        mapper.markReferencedBinaries();
1359    }
1360
1361    public int cleanupDeletedDocuments(int max, Calendar beforeTime) {
1362        checkLive();
1363        if (!repository.getRepositoryDescriptor().getSoftDeleteEnabled()) {
1364            return 0;
1365        }
1366        return mapper.cleanupDeletedRows(max, beforeTime);
1367    }
1368
1369    /*
1370     * ----- XAResource -----
1371     */
1372
1373    @Override
1374    public boolean isSameRM(XAResource xaresource) {
1375        return xaresource == this;
1376    }
1377
1378    @Override
1379    public void start(Xid xid, int flags) throws XAException {
1380        if (flags == TMNOFLAGS) {
1381            try {
1382                processReceivedInvalidations();
1383            } catch (NuxeoException e) {
1384                log.error("Could not start transaction", e);
1385                throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e);
1386            }
1387        }
1388        mapper.start(xid, flags);
1389        inTransaction = true;
1390        checkThreadStart();
1391    }
1392
1393    @Override
1394    public void end(Xid xid, int flags) throws XAException {
1395        boolean failed = true;
1396        try {
1397            if (flags != TMFAIL) {
1398                try {
1399                    flush();
1400                } catch (ConcurrentUpdateException e) {
1401                    TransactionHelper.noteSuppressedException(e);
1402                    log.debug("Exception during transaction commit", e);
1403                    // set rollback only manually instead of throwing, this avoids
1404                    // a spurious log in Geronimo TransactionImpl and has the same effect
1405                    TransactionHelper.setTransactionRollbackOnly();
1406                    return;
1407                } catch (NuxeoException e) {
1408                    log.error("Exception during transaction commit", e);
1409                    throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e);
1410                }
1411            }
1412            failed = false;
1413            mapper.end(xid, flags);
1414        } finally {
1415            if (failed) {
1416                mapper.end(xid, TMFAIL);
1417                // rollback done by tx manager
1418            }
1419        }
1420    }
1421
1422    @Override
1423    public int prepare(Xid xid) throws XAException {
1424        int res = mapper.prepare(xid);
1425        if (res == XA_RDONLY) {
1426            // Read-only optimization, commit() won't be called by the TM.
1427            // It's important to nevertheless send invalidations because
1428            // Oracle, in tightly-coupled transaction mode, can return
1429            // this status even when some changes were actually made
1430            // (they just will be committed by another resource).
1431            // See NXP-7943
1432            commitDone();
1433        }
1434        return res;
1435    }
1436
1437    @Override
1438    public void commit(Xid xid, boolean onePhase) throws XAException {
1439        try {
1440            mapper.commit(xid, onePhase);
1441        } finally {
1442            commitDone();
1443        }
1444    }
1445
1446    protected void commitDone() throws XAException {
1447        inTransaction = false;
1448        try {
1449            try {
1450                sendInvalidationsToOthers();
1451            } finally {
1452                checkThreadEnd();
1453            }
1454        } catch (NuxeoException e) {
1455            log.error("Could not send invalidations", e);
1456            throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e);
1457        }
1458    }
1459
1460    @Override
1461    public void rollback(Xid xid) throws XAException {
1462        try {
1463            try {
1464                mapper.rollback(xid);
1465            } finally {
1466                rollback();
1467            }
1468        } finally {
1469            inTransaction = false;
1470            // no invalidations to send
1471            checkThreadEnd();
1472        }
1473    }
1474
1475    @Override
1476    public void forget(Xid xid) throws XAException {
1477        mapper.forget(xid);
1478    }
1479
1480    @Override
1481    public Xid[] recover(int flag) throws XAException {
1482        return mapper.recover(flag);
1483    }
1484
1485    @Override
1486    public boolean setTransactionTimeout(int seconds) throws XAException {
1487        return mapper.setTransactionTimeout(seconds);
1488    }
1489
1490    @Override
1491    public int getTransactionTimeout() throws XAException {
1492        return mapper.getTransactionTimeout();
1493    }
1494
1495    public long getCacheSize() {
1496        return context.getCacheSize();
1497    }
1498
1499    public long getCacheMapperSize() {
1500        return context.getCacheMapperSize();
1501    }
1502
1503    public long getCachePristineSize() {
1504        return context.getCachePristineSize();
1505    }
1506
1507    public long getCacheSelectionSize() {
1508        return context.getCacheSelectionSize();
1509    }
1510
1511    @Override
1512    public Map<String, String> getBinaryFulltext(Serializable id) {
1513        if (repository.getRepositoryDescriptor().getFulltextDescriptor().getFulltextDisabled()) {
1514            return null;
1515        }
1516        RowId rowId = new RowId(Model.FULLTEXT_TABLE_NAME, id);
1517        return mapper.getBinaryFulltext(rowId);
1518    }
1519
1520}