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}