001/* 002 * (C) Copyright 2006-2017 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 * Bogdan Stefanescu 018 * Florent Guillaume 019 * Kevin Leturc <kleturc@nuxeo.com> 020 */ 021package org.nuxeo.ecm.core.test; 022 023import java.io.IOException; 024import java.io.Serializable; 025import java.net.URL; 026import java.time.Duration; 027import java.util.ArrayList; 028import java.util.List; 029import java.util.Map; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.ecm.core.api.CloseableCoreSession; 034import org.nuxeo.ecm.core.api.CoreInstance; 035import org.nuxeo.ecm.core.api.CoreSession; 036import org.nuxeo.ecm.core.api.CoreSessionService; 037import org.nuxeo.ecm.core.api.CoreSessionService.CoreSessionRegistrationInfo; 038import org.nuxeo.ecm.core.api.DocumentModel; 039import org.nuxeo.ecm.core.api.DocumentRef; 040import org.nuxeo.ecm.core.api.IdRef; 041import org.nuxeo.ecm.core.api.IterableQueryResult; 042import org.nuxeo.ecm.core.api.NuxeoException; 043import org.nuxeo.ecm.core.api.NuxeoPrincipal; 044import org.nuxeo.ecm.core.api.PathRef; 045import org.nuxeo.ecm.core.api.impl.UserPrincipal; 046import org.nuxeo.ecm.core.api.security.ACP; 047import org.nuxeo.ecm.core.bulk.CoreBulkFeature; 048import org.nuxeo.ecm.core.query.QueryParseException; 049import org.nuxeo.ecm.core.query.sql.NXQL; 050import org.nuxeo.ecm.core.repository.RepositoryService; 051import org.nuxeo.ecm.core.test.annotations.Granularity; 052import org.nuxeo.ecm.core.test.annotations.RepositoryConfig; 053import org.nuxeo.ecm.core.test.annotations.RepositoryInit; 054import org.nuxeo.ecm.core.work.WorkManagerFeature; 055import org.nuxeo.runtime.api.Framework; 056import org.nuxeo.runtime.jtajca.NuxeoContainer; 057import org.nuxeo.runtime.model.URLStreamRef; 058import org.nuxeo.runtime.stream.RuntimeStreamFeature; 059import org.nuxeo.runtime.test.runner.Defaults; 060import org.nuxeo.runtime.test.runner.Deploy; 061import org.nuxeo.runtime.test.runner.Features; 062import org.nuxeo.runtime.test.runner.FeaturesRunner; 063import org.nuxeo.runtime.test.runner.HotDeployer; 064import org.nuxeo.runtime.test.runner.RunnerFeature; 065import org.nuxeo.runtime.test.runner.RuntimeFeature; 066import org.nuxeo.runtime.test.runner.RuntimeHarness; 067import org.nuxeo.runtime.test.runner.TransactionalFeature; 068import org.nuxeo.runtime.transaction.TransactionHelper; 069 070import com.google.inject.Binder; 071 072/** 073 * The core feature provides a default {@link CoreSession} that can be injected. 074 * <p> 075 * In addition, by injecting the feature itself, some helper methods are available to open new sessions. 076 */ 077@Deploy("org.nuxeo.runtime.management") 078@Deploy("org.nuxeo.runtime.metrics") 079@Deploy("org.nuxeo.runtime.reload") 080@Deploy("org.nuxeo.runtime.kv") 081@Deploy("org.nuxeo.runtime.pubsub") 082@Deploy("org.nuxeo.runtime.mongodb") 083@Deploy("org.nuxeo.runtime.migration") 084@Deploy("org.nuxeo.runtime.stream") 085@Deploy("org.nuxeo.ecm.core.schema") 086@Deploy("org.nuxeo.ecm.core.query") 087@Deploy("org.nuxeo.ecm.core.api") 088@Deploy("org.nuxeo.ecm.core.event") 089@Deploy("org.nuxeo.ecm.core") 090@Deploy("org.nuxeo.ecm.core.io") 091@Deploy("org.nuxeo.ecm.core.cache") 092@Deploy("org.nuxeo.ecm.core.test") 093@Deploy("org.nuxeo.ecm.core.mimetype") 094@Deploy("org.nuxeo.ecm.core.convert") 095@Deploy("org.nuxeo.ecm.core.convert.plugins") 096@Deploy("org.nuxeo.ecm.core.storage") 097@Deploy("org.nuxeo.ecm.core.storage.sql") 098@Deploy("org.nuxeo.ecm.core.storage.sql.test") 099@Deploy("org.nuxeo.ecm.core.storage.dbs") 100@Deploy("org.nuxeo.ecm.core.storage.mem") 101@Deploy("org.nuxeo.ecm.core.storage.mongodb") 102@Deploy("org.nuxeo.ecm.platform.commandline.executor") 103@Deploy("org.nuxeo.ecm.platform.el") 104@RepositoryConfig(cleanup = Granularity.METHOD) 105@Features({ RuntimeFeature.class, 106 TransactionalFeature.class, 107 RuntimeStreamFeature.class, 108 WorkManagerFeature.class, 109 CoreBulkFeature.class }) 110public class CoreFeature implements RunnerFeature { 111 112 protected ACP rootAcp; 113 114 private static final Log log = LogFactory.getLog(CoreFeature.class); 115 116 protected StorageConfiguration storageConfiguration; 117 118 protected RepositoryInit repositoryInit; 119 120 protected Granularity granularity; 121 122 // this value gets injected 123 protected CoreSession session; 124 125 protected boolean cleaned; 126 127 protected TransactionalFeature txFeature; 128 129 public StorageConfiguration getStorageConfiguration() { 130 return storageConfiguration; 131 } 132 133 @Override 134 public void initialize(FeaturesRunner runner) { 135 runner.getFeature(RuntimeFeature.class).registerHandler(new CoreDeployer()); 136 137 storageConfiguration = new StorageConfiguration(this); 138 txFeature = runner.getFeature(TransactionalFeature.class); 139 // init from RepositoryConfig annotations 140 RepositoryConfig repositoryConfig = runner.getConfig(RepositoryConfig.class); 141 if (repositoryConfig == null) { 142 repositoryConfig = Defaults.of(RepositoryConfig.class); 143 } 144 try { 145 repositoryInit = repositoryConfig.init().newInstance(); 146 } catch (ReflectiveOperationException e) { 147 throw new NuxeoException(e); 148 } 149 Granularity cleanup = repositoryConfig.cleanup(); 150 granularity = cleanup == Granularity.UNDEFINED ? Granularity.CLASS : cleanup; 151 } 152 153 public Granularity getGranularity() { 154 return granularity; 155 } 156 157 @Override 158 public void start(FeaturesRunner runner) { 159 try { 160 RuntimeHarness harness = runner.getFeature(RuntimeFeature.class).getHarness(); 161 storageConfiguration.init(); 162 for (String bundle : storageConfiguration.getExternalBundles()) { 163 try { 164 harness.deployBundle(bundle); 165 } catch (Exception e) { 166 throw new NuxeoException(e); 167 } 168 } 169 URL blobContribUrl = storageConfiguration.getBlobManagerContrib(runner); 170 harness.getContext().deploy(new URLStreamRef(blobContribUrl)); 171 URL repoContribUrl = storageConfiguration.getRepositoryContrib(runner); 172 harness.getContext().deploy(new URLStreamRef(repoContribUrl)); 173 } catch (IOException e) { 174 throw new NuxeoException(e); 175 } 176 } 177 178 @Override 179 public void beforeRun(FeaturesRunner runner) { 180 // wait for async tasks that may have been triggered by 181 // RuntimeFeature (typically repo initialization) 182 txFeature.nextTransaction(Duration.ofSeconds(10)); 183 if (granularity != Granularity.METHOD) { 184 // we need a transaction to properly initialize the session 185 // but it hasn't been started yet by TransactionalFeature 186 TransactionHelper.startTransaction(); 187 initializeSession(runner); 188 TransactionHelper.commitOrRollbackTransaction(); 189 } 190 } 191 192 @Override 193 public void configure(FeaturesRunner runner, Binder binder) { 194 binder.bind(CoreSession.class).toProvider(() -> session); 195 } 196 197 @Override 198 public void afterRun(FeaturesRunner runner) { 199 waitForAsyncCompletion(); // fulltext and various workers 200 if (granularity != Granularity.METHOD) { 201 cleanupSession(runner); 202 } 203 if (session != null) { 204 releaseCoreSession(); 205 } 206 207 List<CoreSessionRegistrationInfo> leakedInfos = Framework.getService(CoreSessionService.class) 208 .getCoreSessionRegistrationInfos(); 209 if (leakedInfos.size() == 0) { 210 return; 211 } 212 AssertionError leakedErrors = new AssertionError(String.format("leaked %d sessions", leakedInfos.size())); 213 for (CoreSessionRegistrationInfo info : leakedInfos) { 214 try { 215 ((CloseableCoreSession) info.getCoreSession()).close(); 216 leakedErrors.addSuppressed(info); 217 } catch (RuntimeException cause) { 218 leakedErrors.addSuppressed(cause); 219 } 220 } 221 throw leakedErrors; 222 } 223 224 @Override 225 public void beforeSetup(FeaturesRunner runner) { 226 if (granularity == Granularity.METHOD) { 227 initializeSession(runner); 228 } 229 } 230 231 @Override 232 public void afterTeardown(FeaturesRunner runner) { 233 if (granularity == Granularity.METHOD) { 234 cleanupSession(runner); 235 } else { 236 waitForAsyncCompletion(); 237 } 238 } 239 240 public void waitForAsyncCompletion() { 241 txFeature.nextTransaction(); 242 } 243 244 protected void cleanupSession(FeaturesRunner runner) { 245 waitForAsyncCompletion(); 246 if (session == null) { 247 createCoreSession(); 248 } 249 TransactionHelper.runInNewTransaction(() -> { 250 try { 251 log.trace("remove everything except root"); 252 // remove proxies first, as we cannot remove a target if there's a proxy pointing to it 253 try (IterableQueryResult results = session.queryAndFetch( 254 "SELECT ecm:uuid FROM Document WHERE ecm:isProxy = 1", NXQL.NXQL)) { 255 batchRemoveDocuments(results); 256 } catch (QueryParseException e) { 257 // ignore, proxies disabled 258 } 259 // remove non-proxies 260 session.removeChildren(new PathRef("/")); 261 log.trace( 262 "remove orphan versions as OrphanVersionRemoverListener is not triggered by CoreSession#removeChildren"); 263 // remove remaining placeless documents 264 try (IterableQueryResult results = session.queryAndFetch("SELECT ecm:uuid FROM Document, Relation", 265 NXQL.NXQL)) { 266 batchRemoveDocuments(results); 267 } 268 // set original ACP on root 269 DocumentModel root = session.getRootDocument(); 270 root.setACP(rootAcp, true); 271 272 session.save(); 273 waitForAsyncCompletion(); 274 if (!session.query("SELECT * FROM Document, Relation").isEmpty()) { 275 log.error("Fail to cleanupSession, repository will not be empty for the next test."); 276 } 277 } catch (NuxeoException e) { 278 log.error("Unable to reset repository", e); 279 } finally { 280 CoreScope.INSTANCE.exit(); 281 } 282 }); 283 releaseCoreSession(); 284 cleaned = true; 285 } 286 287 protected void batchRemoveDocuments(IterableQueryResult results) { 288 String rootDocumentId = session.getRootDocument().getId(); 289 List<DocumentRef> ids = new ArrayList<>(); 290 for (Map<String, Serializable> result : results) { 291 String id = (String) result.get("ecm:uuid"); 292 if (id.equals(rootDocumentId)) { 293 continue; 294 } 295 ids.add(new IdRef(id)); 296 if (ids.size() >= 100) { 297 batchRemoveDocuments(ids); 298 ids.clear(); 299 } 300 } 301 if (!ids.isEmpty()) { 302 batchRemoveDocuments(ids); 303 } 304 } 305 306 protected void batchRemoveDocuments(List<DocumentRef> ids) { 307 List<DocumentRef> deferredIds = new ArrayList<>(); 308 for (DocumentRef id : ids) { 309 if (!session.exists(id)) { 310 continue; 311 } 312 if (session.canRemoveDocument(id)) { 313 session.removeDocument(id); 314 } else { 315 deferredIds.add(id); 316 } 317 } 318 session.removeDocuments(deferredIds.toArray(new DocumentRef[0])); 319 } 320 321 protected void initializeSession(FeaturesRunner runner) { 322 if (cleaned) { 323 // reinitialize repositories content 324 RepositoryService repositoryService = Framework.getService(RepositoryService.class); 325 repositoryService.initRepositories(); 326 cleaned = false; 327 } 328 CoreScope.INSTANCE.enter(); 329 createCoreSession(); 330 if (repositoryInit != null) { 331 repositoryInit.populate(session); 332 session.save(); 333 waitForAsyncCompletion(); 334 } 335 // save current root acp 336 DocumentModel root = session.getRootDocument(); 337 rootAcp = root.getACP(); 338 } 339 340 public String getRepositoryName() { 341 return getStorageConfiguration().getRepositoryName(); 342 } 343 344 public CloseableCoreSession openCoreSession(String username) { 345 return CoreInstance.openCoreSession(getRepositoryName(), username); 346 } 347 348 public CloseableCoreSession openCoreSession(NuxeoPrincipal principal) { 349 return CoreInstance.openCoreSession(getRepositoryName(), principal); 350 } 351 352 public CloseableCoreSession openCoreSession() { 353 return CoreInstance.openCoreSession(getRepositoryName()); 354 } 355 356 public CloseableCoreSession openCoreSessionSystem() { 357 return CoreInstance.openCoreSessionSystem(getRepositoryName()); 358 } 359 360 public CloseableCoreSession createCoreSession() { 361 UserPrincipal principal = new UserPrincipal("Administrator", new ArrayList<>(), false, true); 362 session = CoreInstance.openCoreSession(getRepositoryName(), principal); 363 return (CloseableCoreSession) session; 364 } 365 366 public CoreSession getCoreSession() { 367 return session; 368 } 369 370 public void releaseCoreSession() { 371 ((CloseableCoreSession) session).close(); 372 session = null; 373 } 374 375 public CoreSession reopenCoreSession() { 376 releaseCoreSession(); 377 waitForAsyncCompletion(); 378 // flush JCA cache to acquire a new low-level session 379 NuxeoContainer.resetConnectionManager(); 380 createCoreSession(); 381 return session; 382 } 383 384 public class CoreDeployer extends HotDeployer.ActionHandler { 385 386 @Override 387 public void exec(String action, String... agrs) throws Exception { 388 waitForAsyncCompletion(); 389 releaseCoreSession(); 390 next.exec(action, agrs); 391 createCoreSession(); 392 } 393 394 } 395 396}