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