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