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