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}