/*
* Hibernate OGM, Domain model persistence for NoSQL datastores
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.ogm.datastore.mongodb.utils;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.bson.Document;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.ogm.OgmSessionFactory;
import org.hibernate.ogm.cfg.OgmProperties;
import org.hibernate.ogm.datastore.document.options.AssociationStorageType;
import org.hibernate.ogm.datastore.mongodb.MongoDB;
import org.hibernate.ogm.datastore.mongodb.MongoDBDialect;
import org.hibernate.ogm.datastore.mongodb.configuration.impl.MongoDBConfiguration;
import org.hibernate.ogm.datastore.mongodb.impl.MongoDBDatastoreProvider;
import org.hibernate.ogm.datastore.mongodb.logging.impl.Log;
import org.hibernate.ogm.datastore.mongodb.logging.impl.LoggerFactory;
import org.hibernate.ogm.datastore.spi.DatastoreConfiguration;
import org.hibernate.ogm.datastore.spi.DatastoreProvider;
import org.hibernate.ogm.dialect.spi.GridDialect;
import org.hibernate.ogm.exception.impl.Exceptions;
import org.hibernate.ogm.model.key.spi.EntityKey;
import org.hibernate.ogm.utils.GridDialectTestHelper;
import org.json.JSONException;
import org.skyscreamer.jsonassert.JSONCompare;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.JSONCompareResult;
import com.mongodb.MongoException;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
/**
* @author Guillaume Scheibel <guillaume.scheibel@gmail.com>
* @author Sanne Grinovero <sanne@hibernate.org>
*/
public class MongoDBTestHelper implements GridDialectTestHelper {
private static final Log log = LoggerFactory.getLogger();
static {
// Read host and port from environment variable
// Maven's surefire plugin set it to the string 'null'
String mongoHostName = System.getenv( "MONGODB_HOSTNAME" );
if ( isNotNull( mongoHostName ) ) {
System.getProperties().setProperty( OgmProperties.HOST, mongoHostName );
}
String mongoPort = System.getenv( "MONGODB_PORT" );
if ( isNotNull( mongoPort ) ) {
System.getProperties().setProperty( OgmProperties.PORT, mongoPort );
}
}
private static boolean isNotNull(String mongoHostName) {
return mongoHostName != null && mongoHostName.length() > 0 && ! "null".equals( mongoHostName );
}
@Override
public long getNumberOfEntities(Session session) {
return getNumberOfEntities( session.getSessionFactory() );
}
@Override
public long getNumberOfEntities(SessionFactory sessionFactory) {
MongoDBDatastoreProvider provider = MongoDBTestHelper.getProvider( sessionFactory );
MongoDatabase db = provider.getDatabase();
int count = 0;
for ( String collectionName : getEntityCollections( sessionFactory ) ) {
count += db.getCollection( collectionName ).count();
}
return count;
}
private boolean isSystemCollection(String collectionName) {
return collectionName.startsWith( "system." );
}
@Override
public long getNumberOfAssociations(Session session) {
return getNumberOfAssociations( session.getSessionFactory() );
}
@Override
public long getNumberOfAssociations(SessionFactory sessionFactory) {
long associationCount = getNumberOfAssociationsFromGlobalCollection( sessionFactory );
associationCount += getNumberOfAssociationsFromDedicatedCollections( sessionFactory );
associationCount += getNumberOfEmbeddedAssociations( sessionFactory );
return associationCount;
}
public long getNumberOfAssociationsFromGlobalCollection(SessionFactory sessionFactory) {
MongoDatabase db = getProvider( sessionFactory ).getDatabase();
return db.getCollection( MongoDBConfiguration.DEFAULT_ASSOCIATION_STORE ).count();
}
public long getNumberOfAssociationsFromDedicatedCollections(SessionFactory sessionFactory) {
MongoDatabase db = getProvider( sessionFactory ).getDatabase();
Set<String> associationCollections = getDedicatedAssociationCollections( sessionFactory );
long associationCount = 0;
for ( String collectionName : associationCollections ) {
associationCount += db.getCollection( collectionName ).count();
}
return associationCount;
}
// TODO Use aggregation framework for a more efficient solution; Given that there will only be a few
// test collections/entities, that's good enough for now
public long getNumberOfEmbeddedAssociations(SessionFactory sessionFactory) {
MongoDatabase db = getProvider( sessionFactory ).getDatabase();
long associationCount = 0;
for ( String entityCollection : getEntityCollections( sessionFactory ) ) {
MongoCursor<Document> entities = db.getCollection( entityCollection ).find().iterator();
while ( entities.hasNext() ) {
Document entity = entities.next();
associationCount += getNumberOfEmbeddedAssociations( entity );
}
}
return associationCount;
}
private int getNumberOfEmbeddedAssociations(Document entity) {
int numberOfReferences = 0;
for ( String fieldName : entity.keySet() ) {
Object field = entity.get( fieldName );
if ( isAssociation( field ) ) {
numberOfReferences++;
}
}
return numberOfReferences;
}
private boolean isAssociation(Object field) {
return ( field instanceof List );
}
private Set<String> getEntityCollections(SessionFactory sessionFactory) {
MongoDatabase db = MongoDBTestHelper.getProvider( sessionFactory ).getDatabase();
Set<String> names = new HashSet<>();
MongoCursor<String> collections = db.listCollectionNames().iterator();
while ( collections.hasNext() ) {
String collectionName = collections.next();
if ( !isSystemCollection( collectionName ) &&
!isDedicatedAssociationCollection( collectionName ) &&
!isGlobalAssociationCollection( collectionName ) ) {
names.add( collectionName );
}
}
return names;
}
private Set<String> getDedicatedAssociationCollections(SessionFactory sessionFactory) {
MongoDatabase db = MongoDBTestHelper.getProvider( sessionFactory ).getDatabase();
Set<String> names = new HashSet<>();
MongoCursor<String> collections = db.listCollectionNames().iterator();
while ( collections.hasNext() ) {
String collectionName = collections.next();
if ( isDedicatedAssociationCollection( collectionName ) ) {
names.add( collectionName );
}
}
return names;
}
private boolean isDedicatedAssociationCollection(String collectionName) {
return collectionName.startsWith( MongoDBDialect.ASSOCIATIONS_COLLECTION_PREFIX );
}
private boolean isGlobalAssociationCollection(String collectionName) {
return collectionName.equals( MongoDBConfiguration.DEFAULT_ASSOCIATION_STORE );
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> extractEntityTuple(Session session, EntityKey key) {
MongoDBDatastoreProvider provider = MongoDBTestHelper.getProvider( session.getSessionFactory() );
Document finder = new Document( MongoDBDialect.ID_FIELDNAME, key.getColumnValues()[0] );
Document result = provider.getDatabase().getCollection( key.getTable() ).find( finder ).first();
replaceIdentifierColumnName( result, key );
return DocumentUtil.toMap( result );
}
/**
* The MongoDB dialect replaces the name of the column identifier, so when the tuple is extracted from the db
* we replace the column name of the identifier with the original one.
* We are assuming the identifier is not embedded and is a single property.
*/
private void replaceIdentifierColumnName(Document result, EntityKey key) {
Object idValue = result.get( MongoDBDialect.ID_FIELDNAME );
result.remove( MongoDBDialect.ID_FIELDNAME );
result.put( key.getColumnNames()[0], idValue );
}
@Override
public boolean backendSupportsTransactions() {
return false;
}
private static MongoDBDatastoreProvider getProvider(SessionFactory sessionFactory) {
DatastoreProvider provider = ( (SessionFactoryImplementor) sessionFactory ).getServiceRegistry().getService(
DatastoreProvider.class );
if ( !( MongoDBDatastoreProvider.class.isInstance( provider ) ) ) {
throw new RuntimeException( "Not testing with MongoDB, cannot extract underlying cache" );
}
return MongoDBDatastoreProvider.class.cast( provider );
}
@Override
public void dropSchemaAndDatabase(SessionFactory sessionFactory) {
MongoDBDatastoreProvider provider = getProvider( sessionFactory );
try {
provider.getDatabase().drop();
}
catch ( MongoException ex ) {
throw log.unableToDropDatabase( ex, provider.getDatabase().getName() );
}
}
@Override
public Map<String, String> getAdditionalConfigurationProperties() {
return Collections.emptyMap();
}
@Override
public long getNumberOfAssociations(SessionFactory sessionFactory, AssociationStorageType type) {
switch ( type ) {
case ASSOCIATION_DOCUMENT:
return getNumberOfAssociationsFromGlobalCollection( sessionFactory );
case IN_ENTITY:
return getNumberOfEmbeddedAssociations( sessionFactory );
default:
throw new IllegalArgumentException( "Unexpected association storaget type " + type );
}
}
@Override
public GridDialect getGridDialect(DatastoreProvider datastoreProvider) {
return new MongoDBDialect( (MongoDBDatastoreProvider) datastoreProvider );
}
public static void assertDocument(OgmSessionFactory sessionFactory, String collection, String queryDocument, String expectedDocument) {
assertDocument( sessionFactory, collection, queryDocument, null, expectedDocument );
}
public static void assertDocument(OgmSessionFactory sessionFactory, String collection, String queryDocument, String projectionDocument, String expectedDocument) {
Document finder = Document.parse( queryDocument );
Document fields = projectionDocument != null ? Document.parse( projectionDocument ) : null;
MongoDBDatastoreProvider provider = MongoDBTestHelper.getProvider( sessionFactory );
Document actualDocument = provider.getDatabase().getCollection( collection ).find( finder ).projection( fields ).first();
assertJsonEquals( expectedDocument, actualDocument.toJson() );
}
public static Map<String, Document> getIndexes(OgmSessionFactory sessionFactory, String collection) {
MongoDBDatastoreProvider provider = MongoDBTestHelper.getProvider( sessionFactory );
MongoCursor<Document> indexes = provider.getDatabase().getCollection( collection ).listIndexes().iterator();
Map<String, Document> indexMap = new HashMap<>();
while ( indexes.hasNext() ) {
Document index = indexes.next();
indexMap.put( index.get( "name" ).toString(), index );
}
return indexMap;
}
public static void dropIndexes(OgmSessionFactory sessionFactory, String collection) {
MongoDBDatastoreProvider provider = MongoDBTestHelper.getProvider( sessionFactory );
provider.getDatabase().getCollection( collection ).dropIndexes();
}
public static void assertJsonEquals(String expectedJson, String actualJson) {
try {
JSONCompareResult result = JSONCompare.compareJSON( expectedJson, actualJson, JSONCompareMode.NON_EXTENSIBLE );
if ( result.failed() ) {
throw new AssertionError( result.getMessage() + "; Actual: " + actualJson + "; Expected: " + expectedJson );
}
}
catch (JSONException e) {
Exceptions.<RuntimeException>sneakyThrow( e );
}
}
@Override
public Class<? extends DatastoreConfiguration<?>> getDatastoreConfigurationType() {
return MongoDB.class;
}
}