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 *     Kevin Leturc <kleturc@nuxeo.com>
020 */
021package org.nuxeo.ecm.core.test;
022
023import java.io.IOException;
024import java.io.Serializable;
025import java.net.URL;
026import java.time.Duration;
027import java.util.ArrayList;
028import java.util.List;
029import java.util.Map;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.nuxeo.ecm.core.api.CloseableCoreSession;
034import org.nuxeo.ecm.core.api.CoreInstance;
035import org.nuxeo.ecm.core.api.CoreSession;
036import org.nuxeo.ecm.core.api.CoreSessionService;
037import org.nuxeo.ecm.core.api.CoreSessionService.CoreSessionRegistrationInfo;
038import org.nuxeo.ecm.core.api.DocumentModel;
039import org.nuxeo.ecm.core.api.DocumentRef;
040import org.nuxeo.ecm.core.api.IdRef;
041import org.nuxeo.ecm.core.api.IterableQueryResult;
042import org.nuxeo.ecm.core.api.NuxeoException;
043import org.nuxeo.ecm.core.api.NuxeoPrincipal;
044import org.nuxeo.ecm.core.api.PathRef;
045import org.nuxeo.ecm.core.api.impl.UserPrincipal;
046import org.nuxeo.ecm.core.api.security.ACP;
047import org.nuxeo.ecm.core.bulk.CoreBulkFeature;
048import org.nuxeo.ecm.core.query.QueryParseException;
049import org.nuxeo.ecm.core.query.sql.NXQL;
050import org.nuxeo.ecm.core.repository.RepositoryService;
051import org.nuxeo.ecm.core.test.annotations.Granularity;
052import org.nuxeo.ecm.core.test.annotations.RepositoryConfig;
053import org.nuxeo.ecm.core.test.annotations.RepositoryInit;
054import org.nuxeo.ecm.core.work.WorkManagerFeature;
055import org.nuxeo.runtime.api.Framework;
056import org.nuxeo.runtime.jtajca.NuxeoContainer;
057import org.nuxeo.runtime.model.URLStreamRef;
058import org.nuxeo.runtime.stream.RuntimeStreamFeature;
059import org.nuxeo.runtime.test.runner.Defaults;
060import org.nuxeo.runtime.test.runner.Deploy;
061import org.nuxeo.runtime.test.runner.Features;
062import org.nuxeo.runtime.test.runner.FeaturesRunner;
063import org.nuxeo.runtime.test.runner.HotDeployer;
064import org.nuxeo.runtime.test.runner.RunnerFeature;
065import org.nuxeo.runtime.test.runner.RuntimeFeature;
066import org.nuxeo.runtime.test.runner.RuntimeHarness;
067import org.nuxeo.runtime.test.runner.TransactionalFeature;
068import org.nuxeo.runtime.transaction.TransactionHelper;
069
070import com.google.inject.Binder;
071
072/**
073 * The core feature provides a default {@link CoreSession} that can be injected.
074 * <p>
075 * In addition, by injecting the feature itself, some helper methods are available to open new sessions.
076 */
077@Deploy("org.nuxeo.runtime.management")
078@Deploy("org.nuxeo.runtime.metrics")
079@Deploy("org.nuxeo.runtime.reload")
080@Deploy("org.nuxeo.runtime.kv")
081@Deploy("org.nuxeo.runtime.pubsub")
082@Deploy("org.nuxeo.runtime.mongodb")
083@Deploy("org.nuxeo.runtime.migration")
084@Deploy("org.nuxeo.runtime.stream")
085@Deploy("org.nuxeo.ecm.core.schema")
086@Deploy("org.nuxeo.ecm.core.query")
087@Deploy("org.nuxeo.ecm.core.api")
088@Deploy("org.nuxeo.ecm.core.event")
089@Deploy("org.nuxeo.ecm.core")
090@Deploy("org.nuxeo.ecm.core.io")
091@Deploy("org.nuxeo.ecm.core.cache")
092@Deploy("org.nuxeo.ecm.core.test")
093@Deploy("org.nuxeo.ecm.core.mimetype")
094@Deploy("org.nuxeo.ecm.core.convert")
095@Deploy("org.nuxeo.ecm.core.convert.plugins")
096@Deploy("org.nuxeo.ecm.core.storage")
097@Deploy("org.nuxeo.ecm.core.storage.sql")
098@Deploy("org.nuxeo.ecm.core.storage.sql.test")
099@Deploy("org.nuxeo.ecm.core.storage.dbs")
100@Deploy("org.nuxeo.ecm.core.storage.mem")
101@Deploy("org.nuxeo.ecm.core.storage.mongodb")
102@Deploy("org.nuxeo.ecm.platform.commandline.executor")
103@Deploy("org.nuxeo.ecm.platform.el")
104@RepositoryConfig(cleanup = Granularity.METHOD)
105@Features({ RuntimeFeature.class,
106        TransactionalFeature.class,
107        RuntimeStreamFeature.class,
108        WorkManagerFeature.class,
109        CoreBulkFeature.class })
110public class CoreFeature implements RunnerFeature {
111
112    protected ACP rootAcp;
113
114    private static final Log log = LogFactory.getLog(CoreFeature.class);
115
116    protected StorageConfiguration storageConfiguration;
117
118    protected RepositoryInit repositoryInit;
119
120    protected Granularity granularity;
121
122    // this value gets injected
123    protected CoreSession session;
124
125    protected boolean cleaned;
126
127    protected TransactionalFeature txFeature;
128
129    public StorageConfiguration getStorageConfiguration() {
130        return storageConfiguration;
131    }
132
133    @Override
134    public void initialize(FeaturesRunner runner) {
135        runner.getFeature(RuntimeFeature.class).registerHandler(new CoreDeployer());
136
137        storageConfiguration = new StorageConfiguration(this);
138        txFeature = runner.getFeature(TransactionalFeature.class);
139        // init from RepositoryConfig annotations
140        RepositoryConfig repositoryConfig = runner.getConfig(RepositoryConfig.class);
141        if (repositoryConfig == null) {
142            repositoryConfig = Defaults.of(RepositoryConfig.class);
143        }
144        try {
145            repositoryInit = repositoryConfig.init().newInstance();
146        } catch (ReflectiveOperationException e) {
147            throw new NuxeoException(e);
148        }
149        Granularity cleanup = repositoryConfig.cleanup();
150        granularity = cleanup == Granularity.UNDEFINED ? Granularity.CLASS : cleanup;
151    }
152
153    public Granularity getGranularity() {
154        return granularity;
155    }
156
157    @Override
158    public void start(FeaturesRunner runner) {
159        try {
160            RuntimeHarness harness = runner.getFeature(RuntimeFeature.class).getHarness();
161            storageConfiguration.init();
162            for (String bundle : storageConfiguration.getExternalBundles()) {
163                try {
164                    harness.deployBundle(bundle);
165                } catch (Exception e) {
166                    throw new NuxeoException(e);
167                }
168            }
169            URL blobContribUrl = storageConfiguration.getBlobManagerContrib(runner);
170            harness.getContext().deploy(new URLStreamRef(blobContribUrl));
171            URL repoContribUrl = storageConfiguration.getRepositoryContrib(runner);
172            harness.getContext().deploy(new URLStreamRef(repoContribUrl));
173        } catch (IOException e) {
174            throw new NuxeoException(e);
175        }
176    }
177
178    @Override
179    public void beforeRun(FeaturesRunner runner) {
180        // wait for async tasks that may have been triggered by
181        // RuntimeFeature (typically repo initialization)
182        txFeature.nextTransaction(Duration.ofSeconds(10));
183        if (granularity != Granularity.METHOD) {
184            // we need a transaction to properly initialize the session
185            // but it hasn't been started yet by TransactionalFeature
186            TransactionHelper.startTransaction();
187            initializeSession(runner);
188            TransactionHelper.commitOrRollbackTransaction();
189        }
190    }
191
192    @Override
193    public void configure(FeaturesRunner runner, Binder binder) {
194        binder.bind(CoreSession.class).toProvider(() -> session);
195    }
196
197    @Override
198    public void afterRun(FeaturesRunner runner) {
199        waitForAsyncCompletion(); // fulltext and various workers
200        if (granularity != Granularity.METHOD) {
201            cleanupSession(runner);
202        }
203        if (session != null) {
204            releaseCoreSession();
205        }
206
207        List<CoreSessionRegistrationInfo> leakedInfos = Framework.getService(CoreSessionService.class)
208                                                                 .getCoreSessionRegistrationInfos();
209        if (leakedInfos.size() == 0) {
210            return;
211        }
212        AssertionError leakedErrors = new AssertionError(String.format("leaked %d sessions", leakedInfos.size()));
213        for (CoreSessionRegistrationInfo info : leakedInfos) {
214            try {
215                ((CloseableCoreSession) info.getCoreSession()).close();
216                leakedErrors.addSuppressed(info);
217            } catch (RuntimeException cause) {
218                leakedErrors.addSuppressed(cause);
219            }
220        }
221        throw leakedErrors;
222    }
223
224    @Override
225    public void beforeSetup(FeaturesRunner runner) {
226        if (granularity == Granularity.METHOD) {
227            initializeSession(runner);
228        }
229    }
230
231    @Override
232    public void afterTeardown(FeaturesRunner runner) {
233        if (granularity == Granularity.METHOD) {
234            cleanupSession(runner);
235        } else {
236            waitForAsyncCompletion();
237        }
238    }
239
240    public void waitForAsyncCompletion() {
241        txFeature.nextTransaction();
242    }
243
244    protected void cleanupSession(FeaturesRunner runner) {
245        waitForAsyncCompletion();
246        if (session == null) {
247            createCoreSession();
248        }
249        TransactionHelper.runInNewTransaction(() -> {
250            try {
251                log.trace("remove everything except root");
252                // remove proxies first, as we cannot remove a target if there's a proxy pointing to it
253                try (IterableQueryResult results = session.queryAndFetch(
254                        "SELECT ecm:uuid FROM Document WHERE ecm:isProxy = 1", NXQL.NXQL)) {
255                    batchRemoveDocuments(results);
256                } catch (QueryParseException e) {
257                    // ignore, proxies disabled
258                }
259                // remove non-proxies
260                session.removeChildren(new PathRef("/"));
261                log.trace(
262                        "remove orphan versions as OrphanVersionRemoverListener is not triggered by CoreSession#removeChildren");
263                // remove remaining placeless documents
264                try (IterableQueryResult results = session.queryAndFetch("SELECT ecm:uuid FROM Document, Relation",
265                        NXQL.NXQL)) {
266                    batchRemoveDocuments(results);
267                }
268                // set original ACP on root
269                DocumentModel root = session.getRootDocument();
270                root.setACP(rootAcp, true);
271
272                session.save();
273                waitForAsyncCompletion();
274                if (!session.query("SELECT * FROM Document, Relation").isEmpty()) {
275                    log.error("Fail to cleanupSession, repository will not be empty for the next test.");
276                }
277            } catch (NuxeoException e) {
278                log.error("Unable to reset repository", e);
279            } finally {
280                CoreScope.INSTANCE.exit();
281            }
282        });
283        releaseCoreSession();
284        cleaned = true;
285    }
286
287    protected void batchRemoveDocuments(IterableQueryResult results) {
288        String rootDocumentId = session.getRootDocument().getId();
289        List<DocumentRef> ids = new ArrayList<>();
290        for (Map<String, Serializable> result : results) {
291            String id = (String) result.get("ecm:uuid");
292            if (id.equals(rootDocumentId)) {
293                continue;
294            }
295            ids.add(new IdRef(id));
296            if (ids.size() >= 100) {
297                batchRemoveDocuments(ids);
298                ids.clear();
299            }
300        }
301        if (!ids.isEmpty()) {
302            batchRemoveDocuments(ids);
303        }
304    }
305
306    protected void batchRemoveDocuments(List<DocumentRef> ids) {
307        List<DocumentRef> deferredIds = new ArrayList<>();
308        for (DocumentRef id : ids) {
309            if (!session.exists(id)) {
310                continue;
311            }
312            if (session.canRemoveDocument(id)) {
313                session.removeDocument(id);
314            } else {
315                deferredIds.add(id);
316            }
317        }
318        session.removeDocuments(deferredIds.toArray(new DocumentRef[0]));
319    }
320
321    protected void initializeSession(FeaturesRunner runner) {
322        if (cleaned) {
323            // reinitialize repositories content
324            RepositoryService repositoryService = Framework.getService(RepositoryService.class);
325            repositoryService.initRepositories();
326            cleaned = false;
327        }
328        CoreScope.INSTANCE.enter();
329        createCoreSession();
330        if (repositoryInit != null) {
331            repositoryInit.populate(session);
332            session.save();
333            waitForAsyncCompletion();
334        }
335        // save current root acp
336        DocumentModel root = session.getRootDocument();
337        rootAcp = root.getACP();
338    }
339
340    public String getRepositoryName() {
341        return getStorageConfiguration().getRepositoryName();
342    }
343
344    public CloseableCoreSession openCoreSession(String username) {
345        return CoreInstance.openCoreSession(getRepositoryName(), username);
346    }
347
348    public CloseableCoreSession openCoreSession(NuxeoPrincipal principal) {
349        return CoreInstance.openCoreSession(getRepositoryName(), principal);
350    }
351
352    public CloseableCoreSession openCoreSession() {
353        return CoreInstance.openCoreSession(getRepositoryName());
354    }
355
356    public CloseableCoreSession openCoreSessionSystem() {
357        return CoreInstance.openCoreSessionSystem(getRepositoryName());
358    }
359
360    public CloseableCoreSession createCoreSession() {
361        UserPrincipal principal = new UserPrincipal("Administrator", new ArrayList<>(), false, true);
362        session = CoreInstance.openCoreSession(getRepositoryName(), principal);
363        return (CloseableCoreSession) session;
364    }
365
366    public CoreSession getCoreSession() {
367        return session;
368    }
369
370    public void releaseCoreSession() {
371        ((CloseableCoreSession) session).close();
372        session = null;
373    }
374
375    public CoreSession reopenCoreSession() {
376        releaseCoreSession();
377        waitForAsyncCompletion();
378        // flush JCA cache to acquire a new low-level session
379        NuxeoContainer.resetConnectionManager();
380        createCoreSession();
381        return session;
382    }
383
384    public class CoreDeployer extends HotDeployer.ActionHandler {
385
386        @Override
387        public void exec(String action, String... agrs) throws Exception {
388            waitForAsyncCompletion();
389            releaseCoreSession();
390            next.exec(action, agrs);
391            createCoreSession();
392        }
393
394    }
395
396}