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 return getSession(this); 332 } 333 334 protected Session getSession(DBSRepository repository) { 335 Transaction transaction; 336 try { 337 transaction = TransactionHelper.lookupTransactionManager().getTransaction(); 338 if (transaction == null) { 339 throw new NuxeoException("Missing transaction"); 340 } 341 int status = transaction.getStatus(); 342 if (status != Status.STATUS_ACTIVE && status != Status.STATUS_MARKED_ROLLBACK) { 343 throw new NuxeoException("Transaction in invalid state: " + status); 344 } 345 } catch (SystemException | NamingException e) { 346 throw new NuxeoException("Failed to get transaction", e); 347 } 348 TransactionContext context = transactionContexts.get(transaction); 349 if (context == null) { 350 context = new TransactionContext(transaction, newSession(repository)); 351 context.init(); 352 } 353 return context.newSession(); 354 } 355 356 protected DBSSession newSession(DBSRepository repository) { 357 return new DBSSession(repository); 358 } 359 360 public Map<Transaction, TransactionContext> transactionContexts = new ConcurrentHashMap<>(); 361 362 /** 363 * Context maintained during a transaction, holding the base session used, and all session proxy handles that have 364 * been returned to callers. 365 */ 366 public class TransactionContext implements Synchronization { 367 368 protected final Transaction transaction; 369 370 protected final DBSSession baseSession; 371 372 protected final Set<Session> proxies; 373 374 public TransactionContext(Transaction transaction, DBSSession baseSession) { 375 this.transaction = transaction; 376 this.baseSession = baseSession; 377 proxies = new HashSet<>(); 378 } 379 380 public void init() { 381 transactionContexts.put(transaction, this); 382 begin(); 383 // make sure it's closed (with handles) at transaction end 384 try { 385 transaction.registerSynchronization(this); 386 } catch (RollbackException | SystemException e) { 387 throw new RuntimeException(e); 388 } 389 } 390 391 public Session newSession() { 392 ClassLoader cl = getClass().getClassLoader(); 393 DBSSessionInvoker invoker = new DBSSessionInvoker(this); 394 Session proxy = (Session) Proxy.newProxyInstance(cl, new Class[] { Session.class }, invoker); 395 add(proxy); 396 return proxy; 397 } 398 399 public void add(Session proxy) { 400 proxies.add(proxy); 401 } 402 403 public boolean remove(Object proxy) { 404 return proxies.remove(proxy); 405 } 406 407 public void begin() { 408 baseSession.begin(); 409 } 410 411 @Override 412 public void beforeCompletion() { 413 } 414 415 @Override 416 public void afterCompletion(int status) { 417 if (status == Status.STATUS_COMMITTED) { 418 baseSession.commit(); 419 } else if (status == Status.STATUS_ROLLEDBACK) { 420 baseSession.rollback(); 421 } else { 422 log.error("Unexpected afterCompletion status: " + status); 423 } 424 baseSession.close(); 425 removeTransaction(); 426 } 427 428 protected void removeTransaction() { 429 for (Session proxy : proxies.toArray(new Session[0])) { 430 proxy.close(); // so that users of the session proxy see it's not live anymore 431 } 432 transactionContexts.remove(transaction); 433 } 434 } 435 436 /** 437 * An indirection to a base {@link DBSSession} intercepting {@code close()} to not close the base session until the 438 * transaction itself is closed. 439 */ 440 public static class DBSSessionInvoker implements InvocationHandler { 441 442 private static final String METHOD_HASHCODE = "hashCode"; 443 444 private static final String METHOD_EQUALS = "equals"; 445 446 private static final String METHOD_CLOSE = "close"; 447 448 private static final String METHOD_ISLIVE = "isLive"; 449 450 protected final TransactionContext context; 451 452 protected boolean closed; 453 454 public DBSSessionInvoker(TransactionContext context) { 455 this.context = context; 456 } 457 458 @Override 459 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 460 String methodName = method.getName(); 461 if (methodName.equals(METHOD_HASHCODE)) { 462 return doHashCode(); 463 } 464 if (methodName.equals(METHOD_EQUALS)) { 465 return doEquals(args); 466 } 467 if (methodName.equals(METHOD_CLOSE)) { 468 return doClose(proxy); 469 } 470 if (methodName.equals(METHOD_ISLIVE)) { 471 return doIsLive(); 472 } 473 474 if (closed) { 475 throw new NuxeoException("Cannot use closed connection handle"); 476 } 477 478 try { 479 return method.invoke(context.baseSession, args); 480 } catch (ReflectiveOperationException e) { 481 throw ExceptionUtils.unwrapInvoke(e); 482 } 483 } 484 485 protected Integer doHashCode() { 486 return Integer.valueOf(hashCode()); 487 } 488 489 protected Boolean doEquals(Object[] args) { 490 if (args.length != 1 || args[0] == null) { 491 return FALSE; 492 } 493 Object other = args[0]; 494 if (!(Proxy.isProxyClass(other.getClass()))) { 495 return FALSE; 496 } 497 InvocationHandler otherInvoker = Proxy.getInvocationHandler(other); 498 return Boolean.valueOf(equals(otherInvoker)); 499 } 500 501 protected Object doClose(Object proxy) { 502 closed = true; 503 context.remove(proxy); 504 return null; 505 } 506 507 protected Boolean doIsLive() { 508 if (closed) { 509 return FALSE; 510 } else { 511 return Boolean.valueOf(context.baseSession.isLive()); 512 } 513 } 514 } 515 516}