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