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.Collection; 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.CoreInstance.RegistrationInfo; 035import org.nuxeo.ecm.core.api.CoreSession; 036import org.nuxeo.ecm.core.api.DocumentNotFoundException; 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.ServiceProvider; 063import org.nuxeo.runtime.test.runner.SimpleFeature; 064import org.nuxeo.runtime.transaction.TransactionHelper; 065 066import com.google.inject.Scope; 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}) 091@Features({ RuntimeFeature.class, TransactionalFeature.class }) 092@LocalDeploy("org.nuxeo.ecm.core.event:test-queuing.xml") 093public class CoreFeature extends SimpleFeature { 094 095 private static final Log log = LogFactory.getLog(CoreFeature.class); 096 097 protected StorageConfiguration storageConfiguration; 098 099 protected RepositoryInit repositoryInit; 100 101 protected Granularity granularity; 102 103 // this value gets injected 104 protected CoreSession session; 105 106 protected boolean cleaned; 107 108 protected TransactionalFeature txFeature; 109 110 protected class CoreSessionServiceProvider extends ServiceProvider<CoreSession> { 111 public CoreSessionServiceProvider() { 112 super(CoreSession.class); 113 } 114 115 @Override 116 public Scope getScope() { 117 return CoreScope.INSTANCE; 118 } 119 120 @Override 121 public CoreSession get() { 122 return session; 123 } 124 } 125 126 public StorageConfiguration getStorageConfiguration() { 127 return storageConfiguration; 128 } 129 130 @Override 131 public void initialize(FeaturesRunner runner) { 132 storageConfiguration = new StorageConfiguration(this); 133 txFeature = runner.getFeature(TransactionalFeature.class); 134 txFeature.addWaiter(new Waiter() { 135 136 @Override 137 public boolean await(long deadline) throws InterruptedException { 138 return Framework.getService(WorkManager.class) 139 .awaitCompletion(deadline - System.currentTimeMillis(), TimeUnit.MILLISECONDS); 140 } 141 142 }); 143 runner.getFeature(RuntimeFeature.class).addServiceProvider(new CoreSessionServiceProvider()); 144 // init from RepositoryConfig annotations 145 RepositoryConfig repositoryConfig = runner.getConfig(RepositoryConfig.class); 146 if (repositoryConfig == null) { 147 repositoryConfig = Defaults.of(RepositoryConfig.class); 148 } 149 try { 150 repositoryInit = repositoryConfig.init().newInstance(); 151 } catch (ReflectiveOperationException e) { 152 throw new NuxeoException(e); 153 } 154 Granularity cleanup = repositoryConfig.cleanup(); 155 granularity = cleanup == Granularity.UNDEFINED ? Granularity.CLASS : cleanup; 156 } 157 158 159 public Granularity getGranularity() { 160 return granularity; 161 } 162 163 @Override 164 public void start(FeaturesRunner runner) { 165 try { 166 RuntimeHarness harness = runner.getFeature(RuntimeFeature.class).getHarness(); 167 storageConfiguration.init(); 168 URL blobContribUrl = storageConfiguration.getBlobManagerContrib(runner); 169 harness.getContext().deploy(new URLStreamRef(blobContribUrl)); 170 URL repoContribUrl = storageConfiguration.getRepositoryContrib(runner); 171 harness.getContext().deploy(new URLStreamRef(repoContribUrl)); 172 } catch (IOException e) { 173 throw new NuxeoException(e); 174 } 175 } 176 177 @Override 178 public void beforeRun(FeaturesRunner runner) throws InterruptedException { 179 // wait for async tasks that may have been triggered by 180 // RuntimeFeature (typically repo initialization) 181 txFeature.nextTransaction(10, TimeUnit.SECONDS); 182 if (granularity != Granularity.METHOD) { 183 // we need a transaction to properly initialize the session 184 // but it hasn't been started yet by TransactionalFeature 185 TransactionHelper.startTransaction(); 186 initializeSession(runner); 187 TransactionHelper.commitOrRollbackTransaction(); 188 } 189 } 190 191 @Override 192 public void afterRun(FeaturesRunner runner) { 193 waitForAsyncCompletion(); // fulltext and various workers 194 if (granularity != Granularity.METHOD) { 195 cleanupSession(runner); 196 } 197 if (session != null) { 198 releaseCoreSession(); 199 } 200 201 Collection<RegistrationInfo> leakedInfos = CoreInstance.getInstance().getRegistrationInfos(); 202 if (leakedInfos.size() == 0) { 203 return; 204 } 205 AssertionError leakedErrors = new AssertionError(String.format("leaked %d sessions", leakedInfos.size())); 206 for (RegistrationInfo info:leakedInfos) { 207 try { 208 info.session.close(); 209 leakedErrors.addSuppressed(info); 210 } catch (RuntimeException cause) { 211 leakedErrors.addSuppressed(cause); 212 } 213 } 214 throw leakedErrors; 215 } 216 217 @Override 218 public void beforeSetup(FeaturesRunner runner) { 219 if (granularity == Granularity.METHOD) { 220 initializeSession(runner); 221 } 222 } 223 224 @Override 225 public void afterTeardown(FeaturesRunner runner) { 226 if (granularity == Granularity.METHOD) { 227 cleanupSession(runner); 228 } else { 229 waitForAsyncCompletion(); 230 } 231 } 232 233 protected void waitForAsyncCompletion() { 234 txFeature.nextTransaction(); 235 } 236 237 protected void cleanupSession(FeaturesRunner runner) { 238 waitForAsyncCompletion(); 239 if (TransactionHelper.isTransactionMarkedRollback()) { // ensure tx is 240 // active 241 TransactionHelper.commitOrRollbackTransaction(); 242 TransactionHelper.startTransaction(); 243 } 244 if (session == null) { 245 createCoreSession(); 246 } 247 try { 248 log.trace("remove everything except root"); 249 // remove proxies first, as we cannot remove a target if there's a proxy pointing to it 250 try (IterableQueryResult results = session.queryAndFetch( 251 "SELECT ecm:uuid FROM Document WHERE ecm:isProxy = 1", NXQL.NXQL)) { 252 batchRemoveDocuments(results); 253 } catch (QueryParseException e) { 254 // ignore, proxies disabled 255 } 256 // remove non-proxies 257 session.removeChildren(new PathRef("/")); 258 log.trace("remove orphan versions as OrphanVersionRemoverListener is not triggered by CoreSession#removeChildren"); 259 // remove remaining placeless documents 260 try (IterableQueryResult results = session.queryAndFetch("SELECT ecm:uuid FROM Document, Relation", 261 NXQL.NXQL)) { 262 batchRemoveDocuments(results); 263 } 264 session.save(); 265 waitForAsyncCompletion(); 266 if (!session.query("SELECT * FROM Document, Relation").isEmpty()) { 267 log.error("Fail to cleanupSession, repository will not be empty for the next test."); 268 } 269 } catch (NuxeoException e) { 270 log.error("Unable to reset repository", e); 271 } finally { 272 CoreScope.INSTANCE.exit(); 273 } 274 releaseCoreSession(); 275 cleaned = true; 276 } 277 278 protected void batchRemoveDocuments(IterableQueryResult results) { 279 String rootDocumentId = session.getRootDocument().getId(); 280 List<DocumentRef> ids = new ArrayList<>(); 281 for (Map<String, Serializable> result : results) { 282 String id = (String) result.get("ecm:uuid"); 283 if (id.equals(rootDocumentId)) { 284 continue; 285 } 286 ids.add(new IdRef(id)); 287 if (ids.size() >= 100) { 288 batchRemoveDocuments(ids); 289 ids.clear(); 290 } 291 } 292 if (!ids.isEmpty()) { 293 batchRemoveDocuments(ids); 294 } 295 } 296 297 protected void batchRemoveDocuments(List<DocumentRef> ids) { 298 session.removeDocuments(ids.toArray(new DocumentRef[0])); 299 } 300 301 protected void initializeSession(FeaturesRunner runner) { 302 if (cleaned) { 303 // re-trigger application started 304 RepositoryService repositoryService = Framework.getLocalService(RepositoryService.class); 305 repositoryService.applicationStarted(null); 306 cleaned = false; 307 } 308 CoreScope.INSTANCE.enter(); 309 createCoreSession(); 310 if (repositoryInit != null) { 311 repositoryInit.populate(session); 312 session.save(); 313 waitForAsyncCompletion(); 314 } 315 } 316 317 public String getRepositoryName() { 318 return getStorageConfiguration().getRepositoryName(); 319 } 320 321 public CoreSession openCoreSession(String username) { 322 return CoreInstance.openCoreSession(getRepositoryName(), username); 323 } 324 325 public CoreSession openCoreSession(NuxeoPrincipal principal) { 326 return CoreInstance.openCoreSession(getRepositoryName(), principal); 327 } 328 329 public CoreSession openCoreSession() { 330 return CoreInstance.openCoreSession(getRepositoryName()); 331 } 332 333 public CoreSession openCoreSessionSystem() { 334 return CoreInstance.openCoreSessionSystem(getRepositoryName()); 335 } 336 337 public CoreSession createCoreSession() { 338 UserPrincipal principal = new UserPrincipal("Administrator", new ArrayList<String>(), false, true); 339 session = CoreInstance.openCoreSession(getRepositoryName(), principal); 340 return session; 341 } 342 343 public CoreSession getCoreSession() { 344 return session; 345 } 346 347 public void releaseCoreSession() { 348 session.close(); 349 session = null; 350 } 351 352 public CoreSession reopenCoreSession() { 353 releaseCoreSession(); 354 waitForAsyncCompletion(); 355 // flush JCA cache to acquire a new low-level session 356 NuxeoContainer.resetConnectionManager(); 357 createCoreSession(); 358 return session; 359 } 360 361}