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