/*
* 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.query.parsing.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.hql.ast.origin.hql.resolve.path.PropertyPath;
import org.hibernate.hql.ast.spi.EntityNamesResolver;
import org.hibernate.hql.ast.spi.SingleEntityQueryBuilder;
import org.hibernate.hql.ast.spi.SingleEntityQueryRendererDelegate;
import org.hibernate.ogm.datastore.mongodb.logging.impl.Log;
import org.hibernate.ogm.datastore.mongodb.logging.impl.LoggerFactory;
import org.hibernate.ogm.persister.impl.OgmEntityPersister;
import org.hibernate.ogm.util.impl.StringHelper;
import org.bson.Document;
/**
* Parser delegate which creates MongoDB queries in form of {@link Document}s.
*
* @author Gunnar Morling
*/
public class MongoDBQueryRendererDelegate extends SingleEntityQueryRendererDelegate<Document, MongoDBQueryParsingResult> {
private static final Log log = LoggerFactory.getLogger();
private final SessionFactoryImplementor sessionFactory;
private final MongoDBPropertyHelper propertyHelper;
private Document orderBy;
/*
* The fields for which needs to be aggregated using $unwind when running the query
*/
private List<String> unwinds;
public MongoDBQueryRendererDelegate(SessionFactoryImplementor sessionFactory, EntityNamesResolver entityNames, MongoDBPropertyHelper propertyHelper, Map<String, Object> namedParameters) {
super(
propertyHelper,
entityNames,
SingleEntityQueryBuilder.getInstance( new MongoDBPredicateFactory( propertyHelper ), propertyHelper ),
namedParameters );
this.sessionFactory = sessionFactory;
this.propertyHelper = propertyHelper;
}
@Override
public MongoDBQueryParsingResult getResult() {
OgmEntityPersister entityPersister = (OgmEntityPersister) sessionFactory.getEntityPersister( targetType.getName() );
Document query = appendDiscriminatorClause( entityPersister, builder.build() );
return new MongoDBQueryParsingResult(
targetType,
entityPersister.getTableName(),
query,
getProjectionDocument(),
orderBy,
unwinds );
}
private Document appendDiscriminatorClause(OgmEntityPersister entityPersister, Document query) {
String discriminatorColumnName = entityPersister.getDiscriminatorColumnName();
if ( discriminatorColumnName != null ) {
// InheritanceType.SINGLE_TABLE
Document discriminatorFilter = createDiscriminatorFilter( entityPersister, discriminatorColumnName );
if ( query.keySet().isEmpty() ) {
return discriminatorFilter;
}
else {
return new Document( "$and", Arrays.asList( query, discriminatorFilter ) );
}
}
else if ( entityPersister.hasSubclasses() ) {
// InheritanceType.TABLE_PER_CLASS
@SuppressWarnings("unchecked")
Set<String> subclassEntityNames = entityPersister.getEntityMetamodel().getSubclassEntityNames();
throw log.queriesOnPolymorphicEntitiesAreNotSupportedWithTablePerClass( "MongoDB", subclassEntityNames );
}
return query;
}
private Document createDiscriminatorFilter(OgmEntityPersister entityPersister, String discriminatorColumnName) {
final Object discriminatorValue = entityPersister.getDiscriminatorValue();
Document discriminatorFilter = null;
@SuppressWarnings("unchecked")
Set<String> subclassEntityNames = entityPersister.getEntityMetamodel().getSubclassEntityNames();
if ( subclassEntityNames.size() == 1 ) {
discriminatorFilter = new Document( discriminatorColumnName, discriminatorValue );
}
else {
Set<Object> discriminatorValues = new HashSet<>();
discriminatorValues.add( discriminatorValue );
for ( String subclass : subclassEntityNames ) {
OgmEntityPersister subclassPersister = (OgmEntityPersister) sessionFactory.getEntityPersister( subclass );
Object subDiscriminatorValue = subclassPersister.getDiscriminatorValue();
discriminatorValues.add( subDiscriminatorValue );
}
discriminatorFilter = new Document( discriminatorColumnName, new Document( "$in", discriminatorValues ) );
}
return discriminatorFilter;
}
@Override
public void setPropertyPath(PropertyPath propertyPath) {
if ( status == Status.DEFINING_SELECT ) {
List<String> pathWithoutAlias = resolveAlias( propertyPath );
if ( propertyHelper.isSimpleProperty( pathWithoutAlias ) ) {
projections.add( propertyHelper.getColumnName( targetTypeName, propertyPath.getNodeNamesWithoutAlias() ) );
}
else if ( propertyHelper.isNestedProperty( pathWithoutAlias ) ) {
if ( propertyHelper.isEmbeddedProperty( targetTypeName, pathWithoutAlias ) ) {
String columnName = propertyHelper.getColumnName( targetTypeName, pathWithoutAlias );
projections.add( columnName );
List<String> associationPath = propertyHelper.findAssociationPath( targetTypeName, pathWithoutAlias );
// Currently, it is possible to nest only one association inside an embedded
if ( associationPath != null ) {
if ( unwinds == null ) {
unwinds = new ArrayList<String>();
}
String field = StringHelper.join( associationPath, "." );
if ( !unwinds.contains( field ) ) {
unwinds.add( field );
}
}
}
else {
throw new UnsupportedOperationException( "Selecting associated properties not yet implemented." );
}
}
}
else {
this.propertyPath = propertyPath;
}
}
/**
* Returns the projection columns of the parsed query in form of a {@code Document} as expected by MongoDB.
*
* @return a {@code Document} representing the projections of the query
*/
private Document getProjectionDocument() {
if ( projections.isEmpty() ) {
return null;
}
Document projectionDocument = new Document();
for ( String projection : projections ) {
projectionDocument.put( projection, 1 );
}
return projectionDocument;
}
@Override
protected void addSortField(PropertyPath propertyPath, String collateName, boolean isAscending) {
if ( orderBy == null ) {
orderBy = new Document();
}
String columnName = propertyHelper.getColumnName( targetType, propertyPath.getNodeNamesWithoutAlias() );
// Document is essentially a LinkedHashMap, so in case of several sort keys they'll be evaluated in the
// order they're inserted here, which is the order within the original statement
orderBy.put( columnName, isAscending ? 1 : -1 );
}
}