001/*
002 * Copyright (c) 2006-2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Bogdan Stefanescu
011 *     Florent Guillaume
012 */
013package org.nuxeo.ecm.core.test;
014
015import static org.junit.Assert.assertNotNull;
016
017import java.io.Serializable;
018import java.net.URL;
019import java.util.ArrayList;
020import java.util.Map;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.nuxeo.ecm.core.api.CoreInstance;
025import org.nuxeo.ecm.core.api.CoreSession;
026import org.nuxeo.ecm.core.api.DocumentNotFoundException;
027import org.nuxeo.ecm.core.api.IdRef;
028import org.nuxeo.ecm.core.api.IterableQueryResult;
029import org.nuxeo.ecm.core.api.NuxeoException;
030import org.nuxeo.ecm.core.api.NuxeoPrincipal;
031import org.nuxeo.ecm.core.api.PathRef;
032import org.nuxeo.ecm.core.api.impl.UserPrincipal;
033import org.nuxeo.ecm.core.event.EventService;
034import org.nuxeo.ecm.core.query.sql.NXQL;
035import org.nuxeo.ecm.core.repository.RepositoryFactory;
036import org.nuxeo.ecm.core.repository.RepositoryService;
037import org.nuxeo.ecm.core.storage.sql.DatabaseHelper;
038import org.nuxeo.ecm.core.test.annotations.Granularity;
039import org.nuxeo.ecm.core.test.annotations.RepositoryConfig;
040import org.nuxeo.ecm.core.test.annotations.RepositoryInit;
041import org.nuxeo.osgi.OSGiAdapter;
042import org.nuxeo.runtime.api.Framework;
043import org.nuxeo.runtime.jtajca.NuxeoContainer;
044import org.nuxeo.runtime.model.persistence.Contribution;
045import org.nuxeo.runtime.model.persistence.fs.ContributionLocation;
046import org.nuxeo.runtime.test.runner.Defaults;
047import org.nuxeo.runtime.test.runner.Deploy;
048import org.nuxeo.runtime.test.runner.Features;
049import org.nuxeo.runtime.test.runner.FeaturesRunner;
050import org.nuxeo.runtime.test.runner.LocalDeploy;
051import org.nuxeo.runtime.test.runner.RuntimeFeature;
052import org.nuxeo.runtime.test.runner.RuntimeHarness;
053import org.nuxeo.runtime.test.runner.ServiceProvider;
054import org.nuxeo.runtime.test.runner.SimpleFeature;
055import org.nuxeo.runtime.transaction.TransactionHelper;
056import org.osgi.framework.Bundle;
057
058import com.google.inject.Scope;
059
060/**
061 * The core feature provides a default {@link CoreSession} that can be injected.
062 * <p>
063 * In addition, by injecting the feature itself, some helper methods are available to open new sessions.
064 */
065@Deploy({ "org.nuxeo.runtime.management", //
066        "org.nuxeo.ecm.core.schema", //
067        "org.nuxeo.ecm.core.query", //
068        "org.nuxeo.ecm.core.api", //
069        "org.nuxeo.ecm.core.event", //
070        "org.nuxeo.ecm.core", //
071        "org.nuxeo.ecm.core.mimetype", //
072        "org.nuxeo.ecm.core.convert", //
073        "org.nuxeo.ecm.core.convert.plugins", //
074        "org.nuxeo.ecm.core.storage", //
075        "org.nuxeo.ecm.core.storage.sql", //
076        "org.nuxeo.ecm.core.storage.sql.test" //
077})
078@Features({ RuntimeFeature.class, TransactionalFeature.class })
079@LocalDeploy("org.nuxeo.ecm.core.event:test-queuing.xml")
080public class CoreFeature extends SimpleFeature {
081
082    private static final Log log = LogFactory.getLog(CoreFeature.class);
083
084    protected StorageConfiguration storageConfiguration = new StorageConfiguration();
085
086    protected RepositoryInit repositoryInit;
087
088    protected Granularity granularity;
089
090    protected Class<? extends RepositoryFactory> repositoryFactoryClass;
091
092    protected int initialOpenSessions;
093
094    // this value gets injected
095    protected CoreSession session;
096
097    protected boolean cleaned;
098
099    protected class CoreSessionServiceProvider extends ServiceProvider<CoreSession> {
100        public CoreSessionServiceProvider() {
101            super(CoreSession.class);
102        }
103
104        @Override
105        public Scope getScope() {
106            return CoreScope.INSTANCE;
107        }
108
109        @Override
110        public CoreSession get() {
111            return session;
112        }
113    }
114
115    public StorageConfiguration getStorageConfiguration() {
116        return storageConfiguration;
117    }
118
119    @Override
120    public void initialize(FeaturesRunner runner) {
121        runner.getFeature(RuntimeFeature.class).addServiceProvider(new CoreSessionServiceProvider());
122        // init from RepositoryConfig annotations
123        RepositoryConfig repositoryConfig = runner.getConfig(RepositoryConfig.class);
124        if (repositoryConfig == null) {
125            repositoryConfig = Defaults.of(RepositoryConfig.class);
126        }
127        try {
128            repositoryInit = repositoryConfig.init().newInstance();
129        } catch (ReflectiveOperationException e) {
130            throw new NuxeoException(e);
131        }
132        Granularity cleanup = repositoryConfig.cleanup();
133        granularity = cleanup == Granularity.UNDEFINED ? Granularity.CLASS : cleanup;
134        repositoryFactoryClass = repositoryConfig.repositoryFactoryClass();
135    }
136
137    public Granularity getGranularity() {
138        return granularity;
139    }
140
141    @Override
142    public void start(FeaturesRunner runner) {
143        try {
144            log.info("Deploying a VCS repo implementation");
145            // setup system properties for generic XML extension points
146            DatabaseHelper dbHelper = DatabaseHelper.DATABASE;
147            dbHelper.setUp(repositoryFactoryClass);
148            String contribPath = dbHelper.getDeploymentContrib();
149            RuntimeHarness harness = runner.getFeature(RuntimeFeature.class).getHarness();
150            OSGiAdapter osgi = harness.getOSGiAdapter();
151            Bundle bundle = osgi.getRegistry().getBundle("org.nuxeo.ecm.core.storage.sql.test");
152            URL contribURL = bundle.getEntry(contribPath);
153            assertNotNull("deployment contrib " + contribPath + " not found", contribURL);
154            Contribution contrib = new ContributionLocation(getRepositoryName(), contribURL);
155            harness.getContext().deploy(contrib);
156        } catch (Exception e) {
157            throw new NuxeoException(e);
158        }
159    }
160
161    @Override
162    public void beforeRun(FeaturesRunner runner) {
163        // wait for async tasks that may have been triggered by
164        // RuntimeFeature (typically repo initialization)
165        Framework.getLocalService(EventService.class).waitForAsyncCompletion();
166        final CoreInstance core = CoreInstance.getInstance();
167        initialOpenSessions = core.getNumberOfSessions();
168        if (initialOpenSessions != 0) {
169            log.error(String.format("There are already %s open session(s) before running tests.",
170                    Integer.valueOf(initialOpenSessions)));
171            for (CoreInstance.RegistrationInfo info : core.getRegistrationInfos()) {
172                log.warn("Leaking session", info);
173            }
174        }
175        if (granularity != Granularity.METHOD) {
176            initializeSession(runner);
177        }
178    }
179
180    @Override
181    public void afterRun(FeaturesRunner runner) {
182        waitForAsyncCompletion(); // fulltext and various workers
183        if (granularity != Granularity.METHOD) {
184            cleanupSession(runner);
185        }
186        if (session != null) {
187            releaseCoreSession();
188        }
189
190        final CoreInstance core = CoreInstance.getInstance();
191        int finalOpenSessions = core.getNumberOfSessions();
192        int leakedOpenSessions = finalOpenSessions - initialOpenSessions;
193        if (leakedOpenSessions > 0) {
194            log.error(String.format("There are %s open session(s) at tear down; it seems "
195                    + "the test leaked %s session(s).", Integer.valueOf(finalOpenSessions),
196                    Integer.valueOf(leakedOpenSessions)));
197        }
198    }
199
200    @Override
201    public void beforeSetup(FeaturesRunner runner) {
202        if (granularity == Granularity.METHOD) {
203            initializeSession(runner);
204        }
205    }
206
207    @Override
208    public void afterTeardown(FeaturesRunner runner) {
209        if (granularity == Granularity.METHOD) {
210            cleanupSession(runner);
211        }
212    }
213
214    protected void waitForAsyncCompletion() {
215        boolean tx = TransactionHelper.isTransactionActive();
216        boolean rb = TransactionHelper.isTransactionMarkedRollback();
217        if (tx || rb) {
218            // there may be afterCommit work pending, so we
219            // have to commit the transaction
220            TransactionHelper.commitOrRollbackTransaction();
221        }
222        Framework.getLocalService(EventService.class).waitForAsyncCompletion();
223        if (tx || rb) {
224            // restore previous tx status
225            TransactionHelper.startTransaction();
226            if (rb) {
227                TransactionHelper.setTransactionRollbackOnly();
228            }
229        }
230    }
231
232    protected void cleanupSession(FeaturesRunner runner) {
233        waitForAsyncCompletion();
234        if (TransactionHelper.isTransactionMarkedRollback()) { // ensure tx is
235                                                               // active
236            TransactionHelper.commitOrRollbackTransaction();
237            TransactionHelper.startTransaction();
238        }
239        if (session == null) {
240            createCoreSession();
241        }
242        try {
243            log.trace("remove everything except root");
244            session.removeChildren(new PathRef("/"));
245            log.trace("remove orphan versions as OrphanVersionRemoverListener is not triggered by CoreSession#removeChildren");
246            String rootDocumentId = session.getRootDocument().getId();
247            IterableQueryResult results = session.queryAndFetch("SELECT ecm:uuid FROM Document, Relation", NXQL.NXQL);
248            for (Map<String, Serializable> result : results) {
249                String uuid = result.get("ecm:uuid").toString();
250                if (rootDocumentId != uuid) {
251                    try {
252                        session.removeDocument(new IdRef(uuid));
253                    } catch (DocumentNotFoundException e) {
254                        // could have unknown type in db, ignore
255                    }
256                }
257            }
258            results.close();
259            session.save();
260            waitForAsyncCompletion();
261            if (!session.query("SELECT * FROM Document, Relation").isEmpty()) {
262                log.error("Fail to cleanupSession, repository will not be empty for the next test.");
263            }
264        } catch (NuxeoException e) {
265            log.error("Unable to reset repository", e);
266        } finally {
267            CoreScope.INSTANCE.exit();
268        }
269        releaseCoreSession();
270        cleaned = true;
271        CoreInstance.getInstance().cleanupThisThread();
272    }
273
274    protected void initializeSession(FeaturesRunner runner) {
275        if (cleaned) {
276            // re-trigger application started
277            RepositoryService repositoryService = Framework.getLocalService(RepositoryService.class);
278            repositoryService.applicationStarted(null);
279            cleaned = false;
280        }
281        CoreScope.INSTANCE.enter();
282        createCoreSession();
283        if (repositoryInit != null) {
284            repositoryInit.populate(session);
285            session.save();
286            waitForAsyncCompletion();
287        }
288    }
289
290    public String getRepositoryName() {
291        return storageConfiguration.getRepositoryName();
292    }
293
294    public CoreSession openCoreSession(String username) {
295        return CoreInstance.openCoreSession(getRepositoryName(), username);
296    }
297
298    public CoreSession openCoreSession(NuxeoPrincipal principal) {
299        return CoreInstance.openCoreSession(getRepositoryName(), principal);
300    }
301
302    public CoreSession openCoreSession() {
303        return CoreInstance.openCoreSession(getRepositoryName());
304    }
305
306    public CoreSession openCoreSessionSystem() {
307        return CoreInstance.openCoreSessionSystem(getRepositoryName());
308    }
309
310    public CoreSession createCoreSession() {
311        UserPrincipal principal = new UserPrincipal("Administrator", new ArrayList<String>(), false, true);
312        session = CoreInstance.openCoreSession(getRepositoryName(), principal);
313        return session;
314    }
315
316    public CoreSession getCoreSession() {
317        return session;
318    }
319
320    public void releaseCoreSession() {
321        session.close();
322        session = null;
323    }
324
325    public CoreSession reopenCoreSession() {
326        releaseCoreSession();
327        waitForAsyncCompletion();
328        // flush JCA cache to acquire a new low-level session
329        NuxeoContainer.resetConnectionManager();
330        createCoreSession();
331        return session;
332    }
333
334}