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}