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}