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