001/*
002 * (C) Copyright 2014-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage.dbs;
020
021import static java.lang.Boolean.FALSE;
022
023import java.lang.reflect.InvocationHandler;
024import java.lang.reflect.Method;
025import java.lang.reflect.Proxy;
026import java.util.ArrayDeque;
027import java.util.Collection;
028import java.util.Deque;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.concurrent.ConcurrentHashMap;
035
036import javax.naming.NamingException;
037import javax.resource.spi.ConnectionManager;
038import javax.transaction.RollbackException;
039import javax.transaction.Status;
040import javax.transaction.Synchronization;
041import javax.transaction.SystemException;
042import javax.transaction.Transaction;
043
044import org.apache.commons.lang3.StringUtils;
045import org.apache.commons.logging.Log;
046import org.apache.commons.logging.LogFactory;
047import org.nuxeo.common.utils.ExceptionUtils;
048import org.nuxeo.ecm.core.api.NuxeoException;
049import org.nuxeo.ecm.core.api.security.ACE;
050import org.nuxeo.ecm.core.api.security.SecurityConstants;
051import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
052import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
053import org.nuxeo.ecm.core.blob.BlobManager;
054import org.nuxeo.ecm.core.model.Document;
055import org.nuxeo.ecm.core.model.LockManager;
056import org.nuxeo.ecm.core.model.Session;
057import org.nuxeo.ecm.core.schema.DocumentType;
058import org.nuxeo.ecm.core.schema.SchemaManager;
059import org.nuxeo.ecm.core.schema.TypeConstants;
060import org.nuxeo.ecm.core.schema.types.ComplexType;
061import org.nuxeo.ecm.core.schema.types.CompositeType;
062import org.nuxeo.ecm.core.schema.types.Field;
063import org.nuxeo.ecm.core.schema.types.ListType;
064import org.nuxeo.ecm.core.schema.types.Schema;
065import org.nuxeo.ecm.core.schema.types.Type;
066import org.nuxeo.ecm.core.storage.FulltextConfiguration;
067import org.nuxeo.ecm.core.storage.FulltextDescriptor;
068import org.nuxeo.ecm.core.storage.lock.LockManagerService;
069import org.nuxeo.ecm.core.storage.sql.ra.ConnectionFactoryImpl;
070import org.nuxeo.runtime.api.Framework;
071import org.nuxeo.runtime.jtajca.NuxeoContainer;
072import org.nuxeo.runtime.transaction.TransactionHelper;
073
074/**
075 * Provides sharing behavior for repository sessions and other basic functions.
076 *
077 * @since 5.9.4
078 */
079public abstract class DBSRepositoryBase implements DBSRepository {
080
081    private static final Log log = LogFactory.getLog(DBSRepositoryBase.class);
082
083    public static final String TYPE_ROOT = "Root";
084
085    // change to have deterministic pseudo-UUID generation for debugging
086    protected final boolean DEBUG_UUIDS = false;
087
088    private static final String UUID_ZERO = "00000000-0000-0000-0000-000000000000";
089
090    private static final String UUID_ZERO_DEBUG = "UUID_0";
091
092    /**
093     * Type of id to used for documents.
094     *
095     * @since 8.3
096     */
097    public enum IdType {
098        /** Random UUID stored in a string. */
099        varchar,
100        /** Random UUID stored as a native UUID type. */
101        uuid,
102        /** Integer sequence maintained by the database. */
103        sequence,
104    }
105
106    /** @since 8.3 */
107    protected IdType idType;
108
109    protected final String repositoryName;
110
111    protected final FulltextConfiguration fulltextConfiguration;
112
113    protected final BlobManager blobManager;
114
115    protected LockManager lockManager;
116
117    protected final ConnectionManager cm;
118
119    protected final boolean changeTokenEnabled;
120
121    /**
122     * @since 7.4 : used to know if the LockManager was provided by this repository or externally
123     */
124    protected boolean selfRegisteredLockManager = false;
125
126    public DBSRepositoryBase(ConnectionManager cm, String repositoryName, DBSRepositoryDescriptor descriptor) {
127        this.repositoryName = repositoryName;
128        String idt = descriptor.idType;
129        List<IdType> allowed = getAllowedIdTypes();
130        if (StringUtils.isBlank(idt)) {
131            idt = allowed.get(0).name();
132        }
133        try {
134            idType = IdType.valueOf(idt);
135            if (!allowed.contains(idType)) {
136                throw new IllegalArgumentException();
137            }
138        } catch (IllegalArgumentException e) {
139            throw new NuxeoException("Unknown id type: " + idt + ", allowed: " + allowed);
140        }
141        FulltextDescriptor fulltextDescriptor = descriptor.getFulltextDescriptor();
142        if (fulltextDescriptor.getFulltextDisabled()) {
143            fulltextConfiguration = null;
144        } else {
145            fulltextConfiguration = new FulltextConfiguration(fulltextDescriptor);
146        }
147        this.cm = cm;
148        changeTokenEnabled = descriptor.isChangeTokenEnabled();
149        blobManager = Framework.getService(BlobManager.class);
150        initBlobsPaths();
151        initLockManager();
152    }
153
154    /** Gets the allowed id types for this DBS repository. The first one is the default. */
155    public abstract List<IdType> getAllowedIdTypes();
156
157    @Override
158    public void shutdown() {
159        try {
160            NuxeoContainer.disposeConnectionManager(cm);
161        } catch (RuntimeException e) {
162            LogFactory.getLog(ConnectionFactoryImpl.class)
163                      .warn("cannot dispose connection manager of " + repositoryName);
164        }
165        if (selfRegisteredLockManager) {
166            LockManagerService lms = Framework.getService(LockManagerService.class);
167            if (lms != null) {
168                lms.unregisterLockManager(getLockManagerName());
169            }
170        }
171    }
172
173    @Override
174    public String getName() {
175        return repositoryName;
176    }
177
178    @Override
179    public FulltextConfiguration getFulltextConfiguration() {
180        return fulltextConfiguration;
181    }
182
183    protected String getLockManagerName() {
184        // TODO configure in repo descriptor
185        return getName();
186    }
187
188    protected void initLockManager() {
189        String lockManagerName = getLockManagerName();
190        LockManagerService lockManagerService = Framework.getService(LockManagerService.class);
191        lockManager = lockManagerService.getLockManager(lockManagerName);
192        if (lockManager == null) {
193            // no descriptor, use DBS repository intrinsic lock manager
194            lockManager = this;
195            log.info("Repository " + repositoryName + " using own lock manager");
196            lockManagerService.registerLockManager(lockManagerName, lockManager);
197            selfRegisteredLockManager = true;
198        } else {
199            selfRegisteredLockManager = false;
200            log.info("Repository " + repositoryName + " using lock manager " + lockManager);
201        }
202    }
203
204    @Override
205    public LockManager getLockManager() {
206        return lockManager;
207    }
208
209    protected abstract void initBlobsPaths();
210
211    /** Finds the paths for all blobs in all document types. */
212    protected static abstract class BlobFinder {
213
214        protected final Set<String> schemaDone = new HashSet<>();
215
216        protected final Deque<String> path = new ArrayDeque<>();
217
218        public void visit() {
219            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
220            // document types
221            for (DocumentType docType : schemaManager.getDocumentTypes()) {
222                visitSchemas(docType.getSchemas());
223            }
224            // mixins
225            for (CompositeType type : schemaManager.getFacets()) {
226                visitSchemas(type.getSchemas());
227            }
228        }
229
230        protected void visitSchemas(Collection<Schema> schemas) {
231            for (Schema schema : schemas) {
232                if (schemaDone.add(schema.getName())) {
233                    visitComplexType(schema);
234                }
235            }
236        }
237
238        protected void visitComplexType(ComplexType complexType) {
239            if (TypeConstants.isContentType(complexType)) {
240                recordBlobPath();
241                return;
242            }
243            for (Field field : complexType.getFields()) {
244                visitField(field);
245            }
246        }
247
248        /** Records a blob path, stored in the {@link #path} field. */
249        protected abstract void recordBlobPath();
250
251        protected void visitField(Field field) {
252            Type type = field.getType();
253            if (type.isSimpleType()) {
254                // scalar
255                // assume no bare binary exists
256            } else if (type.isComplexType()) {
257                // complex property
258                String name = field.getName().getPrefixedName();
259                path.addLast(name);
260                visitComplexType((ComplexType) type);
261                path.removeLast();
262            } else {
263                // array or list
264                Type fieldType = ((ListType) type).getFieldType();
265                if (fieldType.isSimpleType()) {
266                    // array
267                    // assume no array of bare binaries exist
268                } else {
269                    // complex list
270                    String name = field.getName().getPrefixedName();
271                    path.addLast(name);
272                    visitComplexType((ComplexType) fieldType);
273                    path.removeLast();
274                }
275            }
276        }
277    }
278
279    /**
280     * Initializes the root and its ACP.
281     */
282    public void initRoot() {
283        Session session = getSession();
284        Document root = session.importDocument(getRootId(), null, "", TYPE_ROOT, new HashMap<>());
285        ACLImpl acl = new ACLImpl();
286        acl.add(new ACE(SecurityConstants.ADMINISTRATORS, SecurityConstants.EVERYTHING, true));
287        acl.add(new ACE(SecurityConstants.ADMINISTRATOR, SecurityConstants.EVERYTHING, true));
288        acl.add(new ACE(SecurityConstants.MEMBERS, SecurityConstants.READ, true));
289        ACPImpl acp = new ACPImpl();
290        acp.addACL(acl);
291        session.setACP(root, acp, true);
292        session.save();
293        session.close();
294        if (TransactionHelper.isTransactionActive()) {
295            TransactionHelper.commitOrRollbackTransaction();
296            TransactionHelper.startTransaction();
297        }
298    }
299
300    @Override
301    public String getRootId() {
302        if (DEBUG_UUIDS) {
303            return UUID_ZERO_DEBUG;
304        }
305        switch (idType) {
306        case varchar:
307        case uuid:
308            return UUID_ZERO;
309        case sequence:
310            return "0";
311        default:
312            throw new UnsupportedOperationException();
313        }
314    }
315
316    @Override
317    public BlobManager getBlobManager() {
318        return blobManager;
319    }
320
321    @Override
322    public boolean isFulltextDisabled() {
323        return fulltextConfiguration == null;
324    }
325
326    @Override
327    public boolean isFulltextSearchDisabled() {
328        return isFulltextDisabled() || fulltextConfiguration.fulltextSearchDisabled;
329    }
330
331    @Override
332    public boolean isChangeTokenEnabled() {
333        return changeTokenEnabled;
334    }
335
336    @Override
337    public int getActiveSessionsCount() {
338        return transactionContexts.size();
339    }
340
341    @Override
342    public Session getSession() {
343        return getSession(this);
344    }
345
346    protected Session getSession(DBSRepository repository) {
347        Transaction transaction;
348        try {
349            transaction = TransactionHelper.lookupTransactionManager().getTransaction();
350            if (transaction == null) {
351                throw new NuxeoException("Missing transaction");
352            }
353            int status = transaction.getStatus();
354            if (status != Status.STATUS_ACTIVE && status != Status.STATUS_MARKED_ROLLBACK) {
355                throw new NuxeoException("Transaction in invalid state: " + status);
356            }
357        } catch (SystemException | NamingException e) {
358            throw new NuxeoException("Failed to get transaction", e);
359        }
360        TransactionContext context = transactionContexts.get(transaction);
361        if (context == null) {
362            context = new TransactionContext(transaction, newSession(repository));
363            context.init();
364        }
365        return context.newSession();
366    }
367
368    protected DBSSession newSession(DBSRepository repository) {
369        return new DBSSession(repository);
370    }
371
372    public Map<Transaction, TransactionContext> transactionContexts = new ConcurrentHashMap<>();
373
374    /**
375     * Context maintained during a transaction, holding the base session used, and all session proxy handles that have
376     * been returned to callers.
377     */
378    public class TransactionContext implements Synchronization {
379
380        protected final Transaction transaction;
381
382        protected final DBSSession baseSession;
383
384        protected final Set<Session> proxies;
385
386        public TransactionContext(Transaction transaction, DBSSession baseSession) {
387            this.transaction = transaction;
388            this.baseSession = baseSession;
389            proxies = new HashSet<>();
390        }
391
392        public void init() {
393            transactionContexts.put(transaction, this);
394            begin();
395            // make sure it's closed (with handles) at transaction end
396            try {
397                transaction.registerSynchronization(this);
398            } catch (RollbackException | SystemException e) {
399                throw new RuntimeException(e);
400            }
401        }
402
403        public Session newSession() {
404            ClassLoader cl = getClass().getClassLoader();
405            DBSSessionInvoker invoker = new DBSSessionInvoker(this);
406            Session proxy = (Session) Proxy.newProxyInstance(cl, new Class[] { Session.class }, invoker);
407            add(proxy);
408            return proxy;
409        }
410
411        public void add(Session proxy) {
412            proxies.add(proxy);
413        }
414
415        public boolean remove(Object proxy) {
416            return proxies.remove(proxy);
417        }
418
419        public void begin() {
420            baseSession.begin();
421        }
422
423        @Override
424        public void beforeCompletion() {
425        }
426
427        @Override
428        public void afterCompletion(int status) {
429            if (status == Status.STATUS_COMMITTED) {
430                baseSession.commit();
431            } else if (status == Status.STATUS_ROLLEDBACK) {
432                baseSession.rollback();
433            } else {
434                log.error("Unexpected afterCompletion status: " + status);
435            }
436            baseSession.close();
437            removeTransaction();
438        }
439
440        protected void removeTransaction() {
441            for (Session proxy : proxies.toArray(new Session[0])) {
442                proxy.close(); // so that users of the session proxy see it's not live anymore
443            }
444            transactionContexts.remove(transaction);
445        }
446    }
447
448    /**
449     * An indirection to a base {@link DBSSession} intercepting {@code close()} to not close the base session until the
450     * transaction itself is closed.
451     */
452    public static class DBSSessionInvoker implements InvocationHandler {
453
454        private static final String METHOD_HASHCODE = "hashCode";
455
456        private static final String METHOD_EQUALS = "equals";
457
458        private static final String METHOD_CLOSE = "close";
459
460        private static final String METHOD_ISLIVE = "isLive";
461
462        protected final TransactionContext context;
463
464        protected boolean closed;
465
466        public DBSSessionInvoker(TransactionContext context) {
467            this.context = context;
468        }
469
470        @Override
471        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
472            String methodName = method.getName();
473            if (methodName.equals(METHOD_HASHCODE)) {
474                return doHashCode();
475            }
476            if (methodName.equals(METHOD_EQUALS)) {
477                return doEquals(args);
478            }
479            if (methodName.equals(METHOD_CLOSE)) {
480                return doClose(proxy);
481            }
482            if (methodName.equals(METHOD_ISLIVE)) {
483                return doIsLive();
484            }
485
486            if (closed) {
487                throw new NuxeoException("Cannot use closed connection handle");
488            }
489
490            try {
491                return method.invoke(context.baseSession, args);
492            } catch (ReflectiveOperationException e) {
493                throw ExceptionUtils.unwrapInvoke(e);
494            }
495        }
496
497        protected Integer doHashCode() {
498            return Integer.valueOf(hashCode());
499        }
500
501        protected Boolean doEquals(Object[] args) {
502            if (args.length != 1 || args[0] == null) {
503                return FALSE;
504            }
505            Object other = args[0];
506            if (!(Proxy.isProxyClass(other.getClass()))) {
507                return FALSE;
508            }
509            InvocationHandler otherInvoker = Proxy.getInvocationHandler(other);
510            return Boolean.valueOf(equals(otherInvoker));
511        }
512
513        protected Object doClose(Object proxy) {
514            closed = true;
515            context.remove(proxy);
516            return null;
517        }
518
519        protected Boolean doIsLive() {
520            if (closed) {
521                return FALSE;
522            } else {
523                return Boolean.valueOf(context.baseSession.isLive());
524            }
525        }
526    }
527
528}