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