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 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.test;
020
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertFalse;
023import static org.junit.Assert.assertNotEquals;
024import static org.junit.Assert.assertNotNull;
025import static org.junit.Assert.assertTrue;
026
027import java.net.URL;
028import java.net.UnknownHostException;
029import java.sql.SQLException;
030import java.util.Arrays;
031import java.util.Calendar;
032import java.util.Collections;
033import java.util.List;
034import java.util.function.BiConsumer;
035
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.storage.dbs.DBSHelper;
040import org.nuxeo.ecm.core.storage.mongodb.MongoDBRepository;
041import org.nuxeo.ecm.core.storage.mongodb.MongoDBRepositoryDescriptor;
042import org.nuxeo.ecm.core.storage.sql.DatabaseDB2;
043import org.nuxeo.ecm.core.storage.sql.DatabaseDerby;
044import org.nuxeo.ecm.core.storage.sql.DatabaseH2;
045import org.nuxeo.ecm.core.storage.sql.DatabaseHelper;
046import org.nuxeo.ecm.core.storage.sql.DatabaseMySQL;
047import org.nuxeo.ecm.core.storage.sql.DatabaseOracle;
048import org.nuxeo.ecm.core.storage.sql.DatabasePostgreSQL;
049import org.nuxeo.ecm.core.storage.sql.DatabaseSQLServer;
050import org.nuxeo.runtime.api.Framework;
051import org.nuxeo.runtime.test.runner.FeaturesRunner;
052import org.nuxeo.runtime.test.runner.RuntimeFeature;
053import org.nuxeo.runtime.test.runner.RuntimeHarness;
054import org.osgi.framework.Bundle;
055
056import com.mongodb.BasicDBObject;
057import com.mongodb.DBCollection;
058import com.mongodb.MongoClient;
059
060/**
061 * Description of the specific capabilities of a repository for tests, and helper methods.
062 *
063 * @since 7.3
064 */
065public class StorageConfiguration {
066
067    private static final Log log = LogFactory.getLog(StorageConfiguration.class);
068
069    public static final String CORE_PROPERTY = "nuxeo.test.core";
070
071    public static final String CORE_VCS = "vcs";
072
073    public static final String CORE_MEM = "mem";
074
075    public static final String CORE_MONGODB = "mongodb";
076
077    public static final String CORE_MARKLOGIC = "marklogic";
078
079    public static final String DEFAULT_CORE = CORE_VCS;
080
081    private static final String MONGODB_SERVER_PROPERTY = "nuxeo.test.mongodb.server";
082
083    private static final String MONGODB_DBNAME_PROPERTY = "nuxeo.test.mongodb.dbname";
084
085    private static final String DEFAULT_MONGODB_SERVER = "localhost:27017";
086
087    private static final String DEFAULT_MONGODB_DBNAME = "unittests";
088
089    private String coreType;
090
091    private boolean isVCS;
092
093    private boolean isDBS;
094
095    private DatabaseHelper databaseHelper;
096
097    private DBSHelper dbsHelper;
098
099    final CoreFeature feature;
100
101    public StorageConfiguration(CoreFeature feature) {
102        coreType = defaultSystemProperty(CORE_PROPERTY, DEFAULT_CORE);
103        this.feature = feature;
104    }
105
106    protected static String defaultSystemProperty(String name, String def) {
107        String value = System.getProperty(name);
108        if (value == null || value.equals("") || value.equals("${" + name + "}")) {
109            System.setProperty(name, value = def);
110        }
111        return value;
112    }
113
114    protected static String defaultProperty(String name, String def) {
115        String value = System.getProperty(name);
116        if (value == null || value.equals("") || value.equals("${" + name + "}")) {
117            value = def;
118        }
119        Framework.getProperties().setProperty(name, value);
120        return value;
121    }
122
123    protected void init() {
124        initJDBC();
125        switch (coreType) {
126        case CORE_VCS:
127            isVCS = true;
128            break;
129        case CORE_MEM:
130            isDBS = true;
131            break;
132        case CORE_MONGODB:
133            isDBS = true;
134            initMongoDB();
135            break;
136        default:
137            isDBS = true;
138            initExternal();
139        }
140    }
141
142    protected void initJDBC() {
143        databaseHelper = DatabaseHelper.DATABASE;
144
145        String msg = "Deploying JDBC using " + databaseHelper.getClass().getSimpleName();
146        // System.out used on purpose, don't remove
147        System.out.println(getClass().getSimpleName() + ": " + msg);
148        log.info(msg);
149
150        // setup system properties for generic XML extension points
151        // this is used both for VCS (org.nuxeo.ecm.core.storage.sql.RepositoryService)
152        // and DataSources (org.nuxeo.runtime.datasource) extension points
153        try {
154            databaseHelper.setUp();
155        } catch (SQLException e) {
156            throw new NuxeoException(e);
157        }
158    }
159
160    protected void initMongoDB() {
161        String server = defaultProperty(MONGODB_SERVER_PROPERTY, DEFAULT_MONGODB_SERVER);
162        String dbname = defaultProperty(MONGODB_DBNAME_PROPERTY, DEFAULT_MONGODB_DBNAME);
163        MongoDBRepositoryDescriptor descriptor = new MongoDBRepositoryDescriptor();
164        descriptor.name = getRepositoryName();
165        descriptor.server = server;
166        descriptor.dbname = dbname;
167        try {
168            clearMongoDB(descriptor);
169        } catch (UnknownHostException e) {
170            throw new NuxeoException(e);
171        }
172    }
173
174    protected void clearMongoDB(MongoDBRepositoryDescriptor descriptor) throws UnknownHostException {
175        MongoClient mongoClient = MongoDBRepository.newMongoClient(descriptor);
176        try {
177            DBCollection coll = MongoDBRepository.getCollection(descriptor, mongoClient);
178            coll.dropIndexes();
179            coll.remove(new BasicDBObject());
180            coll = MongoDBRepository.getCountersCollection(descriptor, mongoClient);
181            coll.dropIndexes();
182            coll.remove(new BasicDBObject());
183        } finally {
184            mongoClient.close();
185        }
186    }
187
188    protected void initExternal() {
189        // Get DBSHelper by reflection
190        String className = String.format("org.nuxeo.ecm.core.storage.%s.DBSHelperImpl", coreType);
191        try {
192            dbsHelper = (DBSHelper) Class.forName(className).newInstance();
193            dbsHelper.init();
194        } catch (ReflectiveOperationException e) {
195            throw new NuxeoException("DBSHelperImpl not found: " + className, e);
196        }
197    }
198
199    public boolean isVCS() {
200        return isVCS;
201    }
202
203    public boolean isVCSH2() {
204        return isVCS && databaseHelper instanceof DatabaseH2;
205    }
206
207    public boolean isVCSDerby() {
208        return isVCS && databaseHelper instanceof DatabaseDerby;
209    }
210
211    public boolean isVCSPostgreSQL() {
212        return isVCS && databaseHelper instanceof DatabasePostgreSQL;
213    }
214
215    public boolean isVCSMySQL() {
216        return isVCS && databaseHelper instanceof DatabaseMySQL;
217    }
218
219    public boolean isVCSOracle() {
220        return isVCS && databaseHelper instanceof DatabaseOracle;
221    }
222
223    public boolean isVCSSQLServer() {
224        return isVCS && databaseHelper instanceof DatabaseSQLServer;
225    }
226
227    public boolean isVCSDB2() {
228        return isVCS && databaseHelper instanceof DatabaseDB2;
229    }
230
231    public boolean isDBS() {
232        return isDBS;
233    }
234
235    public boolean isDBSMem() {
236        return isDBS && CORE_MEM.equals(coreType);
237    }
238
239    public boolean isDBSMongoDB() {
240        return isDBS && CORE_MONGODB.equals(coreType);
241    }
242
243    public boolean isDBSExternal() {
244        return dbsHelper != null;
245    }
246
247    public boolean isDBSMarkLogic() {
248        return isDBS && CORE_MARKLOGIC.equals(coreType);
249    }
250
251    public String getRepositoryName() {
252        return "test";
253    }
254
255    /**
256     * For databases that do asynchronous fulltext indexing, sleep a bit.
257     */
258    public void sleepForFulltext() {
259        if (isVCS()) {
260            databaseHelper.sleepForFulltext();
261        } else {
262            // DBS
263        }
264    }
265
266    /**
267     * For databases that don't have sub-second resolution, sleep a bit to get to the next second.
268     */
269    public void maybeSleepToNextSecond() {
270        if (isVCS()) {
271            databaseHelper.maybeSleepToNextSecond();
272        } else {
273            // DBS
274        }
275        // sleep 1 ms nevertheless to have different timestamps
276        try {
277            Thread.sleep(1);
278        } catch (InterruptedException e) {
279            Thread.currentThread().interrupt(); // restore interrupted status
280            throw new RuntimeException(e);
281        }
282    }
283
284    /**
285     * Checks if the database has sub-second resolution.
286     */
287    public boolean hasSubSecondResolution() {
288        if (isVCS()) {
289            return databaseHelper.hasSubSecondResolution();
290        } else {
291            return true; // DBS
292        }
293    }
294
295    public void waitForAsyncCompletion() {
296        feature.waitForAsyncCompletion();
297    }
298
299    public void waitForFulltextIndexing() {
300        waitForAsyncCompletion();
301        sleepForFulltext();
302    }
303
304    /**
305     * Checks if the database supports multiple fulltext indexes.
306     */
307    public boolean supportsMultipleFulltextIndexes() {
308        if (isVCS()) {
309            return databaseHelper.supportsMultipleFulltextIndexes();
310        } else {
311            return false; // DBS
312        }
313    }
314
315    public List<String> getExternalBundles() {
316        if (isDBSExternal()) {
317            return Arrays.asList(String.format("org.nuxeo.ecm.core.storage.%s", coreType),
318                    String.format("org.nuxeo.ecm.core.storage.%s.test", coreType));
319        }
320        return Collections.emptyList();
321    }
322
323    public URL getBlobManagerContrib(FeaturesRunner runner) {
324        String bundleName = "org.nuxeo.ecm.core.test";
325        String contribPath = "OSGI-INF/test-storage-blob-contrib.xml";
326        RuntimeHarness harness = runner.getFeature(RuntimeFeature.class).getHarness();
327        Bundle bundle = harness.getOSGiAdapter().getRegistry().getBundle(bundleName);
328        URL contribURL = bundle.getEntry(contribPath);
329        assertNotNull("deployment contrib " + contribPath + " not found", contribURL);
330        return contribURL;
331    }
332
333    public URL getRepositoryContrib(FeaturesRunner runner) {
334        String msg;
335        if (isVCS()) {
336            msg = "Deploying a VCS repository";
337        } else if (isDBS()) {
338            msg = "Deploying a DBS repository using " + coreType;
339        } else {
340            throw new NuxeoException("Unkown test configuration (not vcs/dbs)");
341        }
342        // System.out used on purpose, don't remove
343        System.out.println(getClass().getSimpleName() + ": " + msg);
344        log.info(msg);
345
346        String contribPath;
347        String bundleName;
348        if (isVCS()) {
349            bundleName = "org.nuxeo.ecm.core.storage.sql.test";
350            contribPath = databaseHelper.getDeploymentContrib();
351        } else {
352            bundleName = "org.nuxeo.ecm.core.test";
353            if (isDBSMem()) {
354                contribPath = "OSGI-INF/test-storage-repo-mem-contrib.xml";
355            } else if (isDBSMongoDB()) {
356                contribPath = "OSGI-INF/test-storage-repo-mongodb-contrib.xml";
357            } else if (isDBSExternal()) {
358                bundleName = String.format("org.nuxeo.ecm.core.storage.%s.test", coreType);
359                contribPath = "OSGI-INF/test-storage-repo-contrib.xml";
360            } else {
361                throw new NuxeoException("Unkown DBS test configuration (not mem/mongodb)");
362            }
363        }
364        RuntimeHarness harness = runner.getFeature(RuntimeFeature.class).getHarness();
365        Bundle bundle = harness.getOSGiAdapter().getRegistry().getBundle(bundleName);
366        URL contribURL = bundle.getEntry(contribPath);
367        assertNotNull("deployment contrib " + contribPath + " not found", contribURL);
368        return contribURL;
369    }
370
371    public void assertEqualsTimestamp(Calendar expected, Calendar actual) {
372        assertEquals(convertToStoredCalendar(expected), convertToStoredCalendar(actual));
373    }
374
375    public void assertNotEqualsTimestamp(Calendar expected, Calendar actual) {
376        assertNotEquals(convertToStoredCalendar(expected), convertToStoredCalendar(actual));
377    }
378
379    /**
380     * Due to some DB restriction this method could fire a false negative. For example 1001ms is before 1002ms but it's
381     * not the case for MySQL (they're equals).
382     */
383    public void assertBeforeTimestamp(Calendar expected, Calendar actual) {
384        BiConsumer<Calendar, Calendar> assertTrue = (exp, act) -> assertTrue(
385                String.format("expected=%s is not before actual=%s", exp, act), exp.before(act));
386        assertTrue.accept(convertToStoredCalendar(expected), convertToStoredCalendar(actual));
387    }
388
389    public void assertNotBeforeTimestamp(Calendar expected, Calendar actual) {
390        BiConsumer<Calendar, Calendar> assertFalse = (exp, act) -> assertFalse(
391                String.format("expected=%s is before actual=%s", exp, act), exp.before(act));
392        assertFalse.accept(convertToStoredCalendar(expected), convertToStoredCalendar(actual));
393    }
394
395    /**
396     * Due to some DB restriction this method could fire a false negative. For example 1002ms is after 1001ms but it's
397     * not the case for MySQL (they're equals).
398     */
399    public void assertAfterTimestamp(Calendar expected, Calendar actual) {
400        BiConsumer<Calendar, Calendar> assertTrue = (exp, act) -> assertTrue(
401                String.format("expected=%s is not after actual=%s", exp, act), exp.after(act));
402        assertTrue.accept(convertToStoredCalendar(expected), convertToStoredCalendar(actual));
403    }
404
405    public void assertNotAfterTimestamp(Calendar expected, Calendar actual) {
406        BiConsumer<Calendar, Calendar> assertFalse = (exp, act) -> assertFalse(
407                String.format("expected=%s is after actual=%s", exp, act), exp.after(act));
408        assertFalse.accept(convertToStoredCalendar(expected), convertToStoredCalendar(actual));
409    }
410
411    private Calendar convertToStoredCalendar(Calendar calendar) {
412        if (isVCSMySQL() || isVCSSQLServer()) {
413            Calendar result = (Calendar) calendar.clone();
414            result.setTimeInMillis(convertToStoredTimestamp(result.getTimeInMillis()));
415            return result;
416        }
417        return calendar;
418    }
419
420    private long convertToStoredTimestamp(long timestamp) {
421        if (isVCSMySQL()) {
422            return timestamp / 1000 * 1000;
423        } else if (isVCSSQLServer()) {
424            // as datetime in SQL Server are rounded to increments of .000, .003, or .007 seconds
425            // see https://msdn.microsoft.com/en-us/library/aa258277(SQL.80).aspx
426            long milliseconds = timestamp % 10;
427            long newTimestamp = timestamp - milliseconds;
428            if (milliseconds == 9) {
429                newTimestamp += 10;
430            } else if (milliseconds >= 5) {
431                newTimestamp += 7;
432            } else if (milliseconds >= 2) {
433                newTimestamp += 3;
434            }
435            return newTimestamp;
436        }
437        return timestamp;
438    }
439}