/*
* (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.storage.dbs;
import static java.lang.Boolean.FALSE;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.naming.NamingException;
import javax.resource.spi.ConnectionManager;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.ExceptionUtils;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.security.ACE;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
import org.nuxeo.ecm.core.blob.BlobManager;
import org.nuxeo.ecm.core.model.Document;
import org.nuxeo.ecm.core.model.LockManager;
import org.nuxeo.ecm.core.model.Session;
import org.nuxeo.ecm.core.schema.DocumentType;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.schema.TypeConstants;
import org.nuxeo.ecm.core.schema.types.ComplexType;
import org.nuxeo.ecm.core.schema.types.CompositeType;
import org.nuxeo.ecm.core.schema.types.Field;
import org.nuxeo.ecm.core.schema.types.ListType;
import org.nuxeo.ecm.core.schema.types.Schema;
import org.nuxeo.ecm.core.schema.types.Type;
import org.nuxeo.ecm.core.storage.FulltextConfiguration;
import org.nuxeo.ecm.core.storage.FulltextDescriptor;
import org.nuxeo.ecm.core.storage.lock.LockManagerService;
import org.nuxeo.ecm.core.storage.sql.ra.ConnectionFactoryImpl;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.jtajca.NuxeoContainer;
import org.nuxeo.runtime.transaction.TransactionHelper;
/**
* Provides sharing behavior for repository sessions and other basic functions.
*
* @since 5.9.4
*/
public abstract class DBSRepositoryBase implements DBSRepository {
private static final Log log = LogFactory.getLog(DBSRepositoryBase.class);
public static final String TYPE_ROOT = "Root";
// change to have deterministic pseudo-UUID generation for debugging
protected final boolean DEBUG_UUIDS = false;
private static final String UUID_ZERO = "00000000-0000-0000-0000-000000000000";
private static final String UUID_ZERO_DEBUG = "UUID_0";
/**
* Type of id to used for documents.
*
* @since 8.3
*/
public enum IdType {
/** Random UUID stored in a string. */
varchar,
/** Random UUID stored as a native UUID type. */
uuid,
/** Integer sequence maintained by the database. */
sequence,
}
/** @since 8.3 */
protected IdType idType;
protected final String repositoryName;
protected final FulltextConfiguration fulltextConfiguration;
protected final BlobManager blobManager;
protected LockManager lockManager;
protected final ConnectionManager cm;
protected final boolean changeTokenEnabled;
/**
* @since 7.4 : used to know if the LockManager was provided by this repository or externally
*/
protected boolean selfRegisteredLockManager = false;
public DBSRepositoryBase(ConnectionManager cm, String repositoryName, DBSRepositoryDescriptor descriptor) {
this.repositoryName = repositoryName;
String idt = descriptor.idType;
List<IdType> allowed = getAllowedIdTypes();
if (StringUtils.isBlank(idt)) {
idt = allowed.get(0).name();
}
try {
idType = IdType.valueOf(idt);
if (!allowed.contains(idType)) {
throw new IllegalArgumentException();
}
} catch (IllegalArgumentException e) {
throw new NuxeoException("Unknown id type: " + idt + ", allowed: " + allowed);
}
FulltextDescriptor fulltextDescriptor = descriptor.getFulltextDescriptor();
if (fulltextDescriptor.getFulltextDisabled()) {
fulltextConfiguration = null;
} else {
fulltextConfiguration = new FulltextConfiguration(fulltextDescriptor);
}
this.cm = cm;
changeTokenEnabled = descriptor.isChangeTokenEnabled();
blobManager = Framework.getService(BlobManager.class);
initBlobsPaths();
initLockManager();
}
/** Gets the allowed id types for this DBS repository. The first one is the default. */
public abstract List<IdType> getAllowedIdTypes();
@Override
public void shutdown() {
try {
NuxeoContainer.disposeConnectionManager(cm);
} catch (RuntimeException e) {
LogFactory.getLog(ConnectionFactoryImpl.class).warn("cannot dispose connection manager of " + repositoryName);
}
if (selfRegisteredLockManager) {
LockManagerService lms = Framework.getService(LockManagerService.class);
if (lms != null) {
lms.unregisterLockManager(getLockManagerName());
}
}
}
@Override
public String getName() {
return repositoryName;
}
@Override
public FulltextConfiguration getFulltextConfiguration() {
return fulltextConfiguration;
}
protected String getLockManagerName() {
// TODO configure in repo descriptor
return getName();
}
protected void initLockManager() {
String lockManagerName = getLockManagerName();
LockManagerService lockManagerService = Framework.getService(LockManagerService.class);
lockManager = lockManagerService.getLockManager(lockManagerName);
if (lockManager == null) {
// no descriptor, use DBS repository intrinsic lock manager
lockManager = this;
log.info("Repository " + repositoryName + " using own lock manager");
lockManagerService.registerLockManager(lockManagerName, lockManager);
selfRegisteredLockManager = true;
} else {
selfRegisteredLockManager = false;
log.info("Repository " + repositoryName + " using lock manager " + lockManager);
}
}
@Override
public LockManager getLockManager() {
return lockManager;
}
protected abstract void initBlobsPaths();
/** Finds the paths for all blobs in all document types. */
protected static abstract class BlobFinder {
protected final Set<String> schemaDone = new HashSet<>();
protected final Deque<String> path = new ArrayDeque<>();
public void visit() {
SchemaManager schemaManager = Framework.getService(SchemaManager.class);
// document types
for (DocumentType docType : schemaManager.getDocumentTypes()) {
visitSchemas(docType.getSchemas());
}
// mixins
for (CompositeType type : schemaManager.getFacets()) {
visitSchemas(type.getSchemas());
}
}
protected void visitSchemas(Collection<Schema> schemas) {
for (Schema schema : schemas) {
if (schemaDone.add(schema.getName())) {
visitComplexType(schema);
}
}
}
protected void visitComplexType(ComplexType complexType) {
if (TypeConstants.isContentType(complexType)) {
recordBlobPath();
return;
}
for (Field field : complexType.getFields()) {
visitField(field);
}
}
/** Records a blob path, stored in the {@link #path} field. */
protected abstract void recordBlobPath();
protected void visitField(Field field) {
Type type = field.getType();
if (type.isSimpleType()) {
// scalar
// assume no bare binary exists
} else if (type.isComplexType()) {
// complex property
String name = field.getName().getPrefixedName();
path.addLast(name);
visitComplexType((ComplexType) type);
path.removeLast();
} else {
// array or list
Type fieldType = ((ListType) type).getFieldType();
if (fieldType.isSimpleType()) {
// array
// assume no array of bare binaries exist
} else {
// complex list
String name = field.getName().getPrefixedName();
path.addLast(name);
visitComplexType((ComplexType) fieldType);
path.removeLast();
}
}
}
}
/**
* Initializes the root and its ACP.
*/
public void initRoot() {
Session session = getSession();
Document root = session.importDocument(getRootId(), null, "", TYPE_ROOT, new HashMap<String, Serializable>());
ACLImpl acl = new ACLImpl();
acl.add(new ACE(SecurityConstants.ADMINISTRATORS, SecurityConstants.EVERYTHING, true));
acl.add(new ACE(SecurityConstants.ADMINISTRATOR, SecurityConstants.EVERYTHING, true));
acl.add(new ACE(SecurityConstants.MEMBERS, SecurityConstants.READ, true));
ACPImpl acp = new ACPImpl();
acp.addACL(acl);
session.setACP(root, acp, true);
session.save();
session.close();
if (TransactionHelper.isTransactionActive()) {
TransactionHelper.commitOrRollbackTransaction();
TransactionHelper.startTransaction();
}
}
@Override
public String getRootId() {
if (DEBUG_UUIDS) {
return UUID_ZERO_DEBUG;
}
switch (idType) {
case varchar:
case uuid:
return UUID_ZERO;
case sequence:
return "0";
default:
throw new UnsupportedOperationException();
}
}
@Override
public BlobManager getBlobManager() {
return blobManager;
}
@Override
public boolean isFulltextDisabled() {
return fulltextConfiguration == null;
}
@Override
public boolean isChangeTokenEnabled() {
return changeTokenEnabled;
}
@Override
public int getActiveSessionsCount() {
return transactionContexts.size();
}
@Override
public Session getSession() {
return getSession(this);
}
protected Session getSession(DBSRepository repository) {
Transaction transaction;
try {
transaction = TransactionHelper.lookupTransactionManager().getTransaction();
if (transaction == null) {
throw new NuxeoException("Missing transaction");
}
int status = transaction.getStatus();
if (status != Status.STATUS_ACTIVE && status != Status.STATUS_MARKED_ROLLBACK) {
throw new NuxeoException("Transaction in invalid state: " + status);
}
} catch (SystemException | NamingException e) {
throw new NuxeoException("Failed to get transaction", e);
}
TransactionContext context = transactionContexts.get(transaction);
if (context == null) {
context = new TransactionContext(transaction, newSession(repository));
context.init();
}
return context.newSession();
}
protected DBSSession newSession(DBSRepository repository) {
return new DBSSession(repository);
}
public Map<Transaction, TransactionContext> transactionContexts = new ConcurrentHashMap<>();
/**
* Context maintained during a transaction, holding the base session used, and all session proxy handles that have
* been returned to callers.
*/
public class TransactionContext implements Synchronization {
protected final Transaction transaction;
protected final DBSSession baseSession;
protected final Set<Session> proxies;
public TransactionContext(Transaction transaction, DBSSession baseSession) {
this.transaction = transaction;
this.baseSession = baseSession;
proxies = new HashSet<>();
}
public void init() {
transactionContexts.put(transaction, this);
begin();
// make sure it's closed (with handles) at transaction end
try {
transaction.registerSynchronization(this);
} catch (RollbackException | SystemException e) {
throw new RuntimeException(e);
}
}
public Session newSession() {
ClassLoader cl = getClass().getClassLoader();
DBSSessionInvoker invoker = new DBSSessionInvoker(this);
Session proxy = (Session) Proxy.newProxyInstance(cl, new Class[] { Session.class }, invoker);
add(proxy);
return proxy;
}
public void add(Session proxy) {
proxies.add(proxy);
}
public boolean remove(Object proxy) {
return proxies.remove(proxy);
}
public void begin() {
baseSession.begin();
}
@Override
public void beforeCompletion() {
}
@Override
public void afterCompletion(int status) {
if (status == Status.STATUS_COMMITTED) {
baseSession.commit();
} else if (status == Status.STATUS_ROLLEDBACK) {
baseSession.rollback();
} else {
log.error("Unexpected afterCompletion status: " + status);
}
baseSession.close();
removeTransaction();
}
protected void removeTransaction() {
for (Session proxy : proxies.toArray(new Session[0])) {
proxy.close(); // so that users of the session proxy see it's not live anymore
}
transactionContexts.remove(transaction);
}
}
/**
* An indirection to a base {@link DBSSession} intercepting {@code close()} to not close the base session until the
* transaction itself is closed.
*/
public static class DBSSessionInvoker implements InvocationHandler {
private static final String METHOD_HASHCODE = "hashCode";
private static final String METHOD_EQUALS = "equals";
private static final String METHOD_CLOSE = "close";
private static final String METHOD_ISLIVE = "isLive";
protected final TransactionContext context;
protected boolean closed;
public DBSSessionInvoker(TransactionContext context) {
this.context = context;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals(METHOD_HASHCODE)) {
return doHashCode();
}
if (methodName.equals(METHOD_EQUALS)) {
return doEquals(args);
}
if (methodName.equals(METHOD_CLOSE)) {
return doClose(proxy);
}
if (methodName.equals(METHOD_ISLIVE)) {
return doIsLive();
}
if (closed) {
throw new NuxeoException("Cannot use closed connection handle");
}
try {
return method.invoke(context.baseSession, args);
} catch (ReflectiveOperationException e) {
throw ExceptionUtils.unwrapInvoke(e);
}
}
protected Integer doHashCode() {
return Integer.valueOf(hashCode());
}
protected Boolean doEquals(Object[] args) {
if (args.length != 1 || args[0] == null) {
return FALSE;
}
Object other = args[0];
if (!(Proxy.isProxyClass(other.getClass()))) {
return FALSE;
}
InvocationHandler otherInvoker = Proxy.getInvocationHandler(other);
return Boolean.valueOf(equals(otherInvoker));
}
protected Object doClose(Object proxy) {
closed = true;
context.remove(proxy);
return null;
}
protected Boolean doIsLive() {
if (closed) {
return FALSE;
} else {
return Boolean.valueOf(context.baseSession.isLive());
}
}
}
}