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