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}