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