001/*
002 * Copyright (c) 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Florent Guillaume
011 */
012package org.nuxeo.ecm.core.storage.dbs;
013
014import static java.lang.Boolean.FALSE;
015
016import java.io.Serializable;
017import java.lang.reflect.InvocationHandler;
018import java.lang.reflect.Method;
019import java.lang.reflect.Proxy;
020import java.util.ArrayDeque;
021import java.util.Collection;
022import java.util.Deque;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Map;
026import java.util.Set;
027import java.util.concurrent.ConcurrentHashMap;
028
029import javax.naming.NamingException;
030import javax.transaction.RollbackException;
031import javax.transaction.Status;
032import javax.transaction.Synchronization;
033import javax.transaction.SystemException;
034import javax.transaction.Transaction;
035
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.nuxeo.common.utils.ExceptionUtils;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.ecm.core.api.security.ACE;
041import org.nuxeo.ecm.core.api.security.SecurityConstants;
042import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
043import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
044import org.nuxeo.ecm.core.blob.BlobManager;
045import org.nuxeo.ecm.core.model.Document;
046import org.nuxeo.ecm.core.model.LockManager;
047import org.nuxeo.ecm.core.model.Session;
048import org.nuxeo.ecm.core.schema.DocumentType;
049import org.nuxeo.ecm.core.schema.SchemaManager;
050import org.nuxeo.ecm.core.schema.TypeConstants;
051import org.nuxeo.ecm.core.schema.types.ComplexType;
052import org.nuxeo.ecm.core.schema.types.CompositeType;
053import org.nuxeo.ecm.core.schema.types.Field;
054import org.nuxeo.ecm.core.schema.types.ListType;
055import org.nuxeo.ecm.core.schema.types.Schema;
056import org.nuxeo.ecm.core.schema.types.Type;
057import org.nuxeo.ecm.core.storage.lock.LockManagerService;
058import org.nuxeo.runtime.api.Framework;
059import org.nuxeo.runtime.transaction.TransactionHelper;
060
061/**
062 * Provides sharing behavior for repository sessions and other basic functions.
063 *
064 * @since 5.9.4
065 */
066public abstract class DBSRepositoryBase implements DBSRepository {
067
068    private static final Log log = LogFactory.getLog(DBSRepositoryBase.class);
069
070    public static final String TYPE_ROOT = "Root";
071
072    // change to have deterministic pseudo-UUID generation for debugging
073    protected final boolean DEBUG_UUIDS = false;
074
075    private static final String UUID_ZERO = "00000000-0000-0000-0000-000000000000";
076
077    private static final String UUID_ZERO_DEBUG = "UUID_0";
078
079    protected final String repositoryName;
080
081    protected final boolean fulltextDisabled;
082
083    protected final BlobManager blobManager;
084
085    protected LockManager lockManager;
086
087    /**
088     * @since 7.4 : used to know if the LockManager was provided by this repository or externally
089     */
090    protected boolean selfRegisteredLockManager = false;
091
092    public DBSRepositoryBase(String repositoryName, boolean fulltextDisabled) {
093        this.repositoryName = repositoryName;
094        this.fulltextDisabled = fulltextDisabled;
095        blobManager = Framework.getService(BlobManager.class);
096        initBlobsPaths();
097        initLockManager();
098    }
099
100    @Override
101    public void shutdown() {
102        if (selfRegisteredLockManager) {
103            LockManagerService lms = Framework.getService(LockManagerService.class);
104            if (lms != null) {
105                lms.unregisterLockManager(getLockManagerName());
106            }
107        }
108    }
109
110    @Override
111    public String getName() {
112        return repositoryName;
113    }
114
115    protected String getLockManagerName() {
116        // TODO configure in repo descriptor
117        return getName();
118    }
119
120    protected void initLockManager() {
121        String lockManagerName = getLockManagerName();
122        LockManagerService lockManagerService = Framework.getService(LockManagerService.class);
123        lockManager = lockManagerService.getLockManager(lockManagerName);
124        if (lockManager == null) {
125            // no descriptor, use DBS repository intrinsic lock manager
126            lockManager = this;
127            log.info("Repository " + repositoryName + " using own lock manager");
128            lockManagerService.registerLockManager(lockManagerName, lockManager);
129            selfRegisteredLockManager = true;
130        } else {
131            selfRegisteredLockManager = false;
132            log.info("Repository " + repositoryName + " using lock manager " + lockManager);
133        }
134    }
135
136    @Override
137    public LockManager getLockManager() {
138        return lockManager;
139    }
140
141    protected abstract void initBlobsPaths();
142
143    /** Finds the paths for all blobs in all document types. */
144    protected static abstract class BlobFinder {
145
146        protected final Set<String> schemaDone = new HashSet<>();
147
148        protected final Deque<String> path = new ArrayDeque<>();
149
150        public void visit() {
151            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
152            // document types
153            for (DocumentType docType : schemaManager.getDocumentTypes()) {
154                visitSchemas(docType.getSchemas());
155            }
156            // mixins
157            for (CompositeType type : schemaManager.getFacets()) {
158                visitSchemas(type.getSchemas());
159            }
160        }
161
162        protected void visitSchemas(Collection<Schema> schemas) {
163            for (Schema schema : schemas) {
164                if (schemaDone.add(schema.getName())) {
165                    visitComplexType(schema);
166                }
167            }
168        }
169
170        protected void visitComplexType(ComplexType complexType) {
171            if (TypeConstants.isContentType(complexType)) {
172                recordBlobPath();
173                return;
174            }
175            for (Field field : complexType.getFields()) {
176                visitField(field);
177            }
178        }
179
180        /** Records a blob path, stored in the {@link #path} field. */
181        protected abstract void recordBlobPath();
182
183        protected void visitField(Field field) {
184            Type type = field.getType();
185            if (type.isSimpleType()) {
186                // scalar
187                // assume no bare binary exists
188            } else if (type.isComplexType()) {
189                // complex property
190                String name = field.getName().getPrefixedName();
191                path.addLast(name);
192                visitComplexType((ComplexType) type);
193                path.removeLast();
194            } else {
195                // array or list
196                Type fieldType = ((ListType) type).getFieldType();
197                if (fieldType.isSimpleType()) {
198                    // array
199                    // assume no array of bare binaries exist
200                } else {
201                    // complex list
202                    String name = field.getName().getPrefixedName();
203                    path.addLast(name);
204                    visitComplexType((ComplexType) fieldType);
205                    path.removeLast();
206                }
207            }
208        }
209    }
210
211    /**
212     * Initializes the root and its ACP.
213     */
214    public void initRoot() {
215        Session session = getSession();
216        Document root = session.importDocument(getRootId(), null, "", TYPE_ROOT, new HashMap<String, Serializable>());
217        ACLImpl acl = new ACLImpl();
218        acl.add(new ACE(SecurityConstants.ADMINISTRATORS, SecurityConstants.EVERYTHING, true));
219        acl.add(new ACE(SecurityConstants.ADMINISTRATOR, SecurityConstants.EVERYTHING, true));
220        acl.add(new ACE(SecurityConstants.MEMBERS, SecurityConstants.READ, true));
221        ACPImpl acp = new ACPImpl();
222        acp.addACL(acl);
223        session.setACP(root, acp, true);
224        session.save();
225        session.close();
226        if (TransactionHelper.isTransactionActive()) {
227            TransactionHelper.commitOrRollbackTransaction();
228            TransactionHelper.startTransaction();
229        }
230    }
231
232    @Override
233    public String getRootId() {
234        return DEBUG_UUIDS ? UUID_ZERO_DEBUG : UUID_ZERO;
235    }
236
237    @Override
238    public BlobManager getBlobManager() {
239        return blobManager;
240    }
241
242    @Override
243    public boolean isFulltextDisabled() {
244        return fulltextDisabled;
245    }
246
247    @Override
248    public int getActiveSessionsCount() {
249        return 0;
250    }
251
252    @Override
253    public Session getSession() {
254        Transaction transaction;
255        try {
256            transaction = TransactionHelper.lookupTransactionManager().getTransaction();
257            if (transaction != null && transaction.getStatus() != Status.STATUS_ACTIVE) {
258                transaction = null;
259            }
260        } catch (SystemException | NamingException e) {
261            transaction = null;
262        }
263
264        if (transaction == null) {
265            // no active transaction, use a regular session
266            return newSession();
267        }
268
269        TransactionContext context = transactionContexts.get(transaction);
270        if (context == null) {
271            context = new TransactionContext(transaction, newSession());
272            context.init();
273        }
274        return context.newSession();
275    }
276
277    protected DBSSession newSession() {
278        return new DBSSession(this);
279    }
280
281    public Map<Transaction, TransactionContext> transactionContexts = new ConcurrentHashMap<>();
282
283    /**
284     * Context maintained during a transaction, holding the base session used, and all session proxy handles that have
285     * been returned to callers.
286     */
287    public class TransactionContext implements Synchronization {
288
289        protected final Transaction transaction;
290
291        protected final DBSSession baseSession;
292
293        protected final Set<Session> proxies;
294
295        public TransactionContext(Transaction transaction, DBSSession baseSession) {
296            this.transaction = transaction;
297            this.baseSession = baseSession;
298            proxies = new HashSet<>();
299        }
300
301        public void init() {
302            transactionContexts.put(transaction, this);
303            begin();
304            // make sure it's closed (with handles) at transaction end
305            try {
306                transaction.registerSynchronization(this);
307            } catch (RollbackException | SystemException e) {
308                throw new RuntimeException(e);
309            }
310        }
311
312        public Session newSession() {
313            ClassLoader cl = getClass().getClassLoader();
314            DBSSessionInvoker invoker = new DBSSessionInvoker(this);
315            Session proxy = (Session) Proxy.newProxyInstance(cl, new Class[] { Session.class }, invoker);
316            add(proxy);
317            return proxy;
318        }
319
320        public void add(Session proxy) {
321            proxies.add(proxy);
322        }
323
324        public boolean remove(Object proxy) {
325            return proxies.remove(proxy);
326        }
327
328        public void begin() {
329            baseSession.begin();
330        }
331
332        @Override
333        public void beforeCompletion() {
334        }
335
336        @Override
337        public void afterCompletion(int status) {
338            if (status == Status.STATUS_COMMITTED) {
339                baseSession.commit();
340            } else if (status == Status.STATUS_ROLLEDBACK) {
341                baseSession.rollback();
342            } else {
343                log.error("Unexpected afterCompletion status: " + status);
344            }
345            baseSession.close();
346            removeTransaction();
347        }
348
349        protected void removeTransaction() {
350            for (Session proxy : proxies.toArray(new Session[0])) {
351                proxy.close(); // so that users of the session proxy see it's not live anymore
352            }
353            transactionContexts.remove(transaction);
354        }
355    }
356
357    /**
358     * An indirection to a base {@link DBSSession} intercepting {@code close()} to not close the base session until the
359     * transaction itself is closed.
360     */
361    public static class DBSSessionInvoker implements InvocationHandler {
362
363        private static final String METHOD_HASHCODE = "hashCode";
364
365        private static final String METHOD_EQUALS = "equals";
366
367        private static final String METHOD_CLOSE = "close";
368
369        private static final String METHOD_ISLIVE = "isLive";
370
371        protected final TransactionContext context;
372
373        protected boolean closed;
374
375        public DBSSessionInvoker(TransactionContext context) {
376            this.context = context;
377        }
378
379        @Override
380        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
381            String methodName = method.getName();
382            if (methodName.equals(METHOD_HASHCODE)) {
383                return doHashCode();
384            }
385            if (methodName.equals(METHOD_EQUALS)) {
386                return doEquals(args);
387            }
388            if (methodName.equals(METHOD_CLOSE)) {
389                return doClose(proxy);
390            }
391            if (methodName.equals(METHOD_ISLIVE)) {
392                return doIsLive();
393            }
394
395            if (closed) {
396                throw new NuxeoException("Cannot use closed connection handle");
397            }
398
399            try {
400                return method.invoke(context.baseSession, args);
401            } catch (ReflectiveOperationException e) {
402                throw ExceptionUtils.unwrapInvoke(e);
403            }
404        }
405
406        protected Integer doHashCode() {
407            return Integer.valueOf(hashCode());
408        }
409
410        protected Boolean doEquals(Object[] args) {
411            if (args.length != 1 || args[0] == null) {
412                return FALSE;
413            }
414            Object other = args[0];
415            if (!(Proxy.isProxyClass(other.getClass()))) {
416                return FALSE;
417            }
418            InvocationHandler otherInvoker = Proxy.getInvocationHandler(other);
419            return Boolean.valueOf(equals(otherInvoker));
420        }
421
422        protected Object doClose(Object proxy) {
423            closed = true;
424            context.remove(proxy);
425            return null;
426        }
427
428        protected Boolean doIsLive() {
429            if (closed) {
430                return FALSE;
431            } else {
432                return Boolean.valueOf(context.baseSession.isLive());
433            }
434        }
435    }
436
437}