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}