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