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}