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