/*
* 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.neo4j.query.parsing.impl;
import static org.hibernate.ogm.datastore.neo4j.query.parsing.cypherdsl.impl.CypherDSL.identifier;
import static org.hibernate.ogm.datastore.neo4j.query.parsing.cypherdsl.impl.CypherDSL.node;
import static org.hibernate.ogm.datastore.neo4j.query.parsing.cypherdsl.impl.CypherDSL.relationship;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.antlr.runtime.tree.Tree;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.hql.ast.common.JoinType;
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.hql.ast.spi.predicate.ComparisonPredicate.Type;
import org.hibernate.ogm.datastore.neo4j.logging.impl.Log;
import org.hibernate.ogm.datastore.neo4j.logging.impl.LoggerFactory;
import org.hibernate.ogm.datastore.neo4j.query.parsing.cypherdsl.impl.CypherDSL;
import org.hibernate.ogm.datastore.neo4j.query.parsing.impl.predicate.impl.Neo4jPredicateFactory;
import org.hibernate.ogm.model.key.spi.EntityKeyMetadata;
import org.hibernate.ogm.persister.impl.OgmEntityPersister;
import org.hibernate.ogm.type.spi.GridType;
import org.hibernate.ogm.type.spi.TypeTranslator;
/**
* Parser delegate which creates Neo4j queries in form of {@link StringBuilder}s.
*
* @author Davide D'Alto
* @author Guillaume Smet
*/
public class Neo4jQueryRendererDelegate extends SingleEntityQueryRendererDelegate<StringBuilder, Neo4jQueryParsingResult> {
private static final Log LOG = LoggerFactory.getLogger();
private final Neo4jPropertyHelper propertyHelper;
private final SessionFactoryImplementor sessionFactory;
private final Neo4jAliasResolver aliasResolver;
private List<OrderByClause> orderByExpressions;
/**
* Temporary state used while parsing the query.
*/
private JoinType joinType;
public Neo4jQueryRendererDelegate(SessionFactoryImplementor sessionFactory, Neo4jAliasResolver aliasResolver, EntityNamesResolver entityNames, Neo4jPropertyHelper propertyHelper, Map<String, Object> namedParameters) {
super( propertyHelper, entityNames, singleEntityQueryBuilder( propertyHelper ), namedParameters );
this.sessionFactory = sessionFactory;
this.aliasResolver = aliasResolver;
this.propertyHelper = propertyHelper;
}
private static SingleEntityQueryBuilder<StringBuilder> singleEntityQueryBuilder(Neo4jPropertyHelper propertyHelper) {
return SingleEntityQueryBuilder.getInstance(
new Neo4jPredicateFactory( propertyHelper ),
propertyHelper
);
}
private EntityKeyMetadata getKeyMetaData(Class<?> entityType) {
return getEntityPersister( entityType ).getEntityKeyMetadata();
}
private OgmEntityPersister getEntityPersister(Class<?> entityType) {
OgmEntityPersister persister = (OgmEntityPersister) ( sessionFactory ).getEntityPersister( entityType.getName() );
return persister;
}
@Override
public Neo4jQueryParsingResult getResult() {
String targetAlias = aliasResolver.findAliasForType( targetTypeName );
String label = getKeyMetaData( targetType ).getTable();
StringBuilder queryBuilder = new StringBuilder();
match( queryBuilder, targetAlias, label );
where( queryBuilder, targetAlias );
optionalMatch( queryBuilder, targetAlias );
returns( queryBuilder, targetAlias );
orderBy( queryBuilder );
return new Neo4jQueryParsingResult( targetType, projections, queryBuilder.toString() );
}
private void match(StringBuilder queryBuilder, String targetAlias, String label) {
queryBuilder.append( "MATCH " );
node( queryBuilder, targetAlias, label );
RelationshipAliasTree node = aliasResolver.getRelationshipAliasTree( targetAlias );
boolean first = true;
if ( node != null ) {
for ( RelationshipAliasTree child : node.getChildren() ) {
if ( !aliasResolver.isOptionalMatch( child.getAlias() ) ) {
StringBuilder relationshipMatch = new StringBuilder();
if ( first ) {
first = false;
}
else {
relationshipMatch.append( ", " );
node( relationshipMatch, targetAlias, label );
}
relationship( relationshipMatch, child.getRelationshipName() );
node( relationshipMatch, child.getAlias(), child.getTargetEntityName() );
appendMatchRelationship( queryBuilder, relationshipMatch.toString(), child );
}
}
}
}
private void appendMatchRelationship(StringBuilder queryBuilder, String currentMatch, RelationshipAliasTree node) {
if ( node.getChildren().isEmpty() ) {
queryBuilder.append( currentMatch );
}
else {
for ( RelationshipAliasTree child : node.getChildren() ) {
boolean optional = aliasResolver.isOptionalMatch( child.getAlias() );
if ( !optional ) {
StringBuilder builder = new StringBuilder( currentMatch );
relationship( builder, child.getRelationshipName() );
node( builder, child.getAlias(), child.getTargetEntityName() );
appendMatchRelationship( queryBuilder, builder.toString(), child );
}
else {
queryBuilder.append( currentMatch );
}
}
}
}
private void optionalMatch(StringBuilder queryBuilder, String targetAlias) {
RelationshipAliasTree node = aliasResolver.getRelationshipAliasTree( targetAlias );
if ( node != null ) {
appendOptionalMatch( queryBuilder, targetAlias, node.getChildren() );
}
}
private void appendOptionalMatch(StringBuilder queryBuilder, String targetAlias, List<RelationshipAliasTree> children) {
for ( RelationshipAliasTree child : children ) {
if ( aliasResolver.isOptionalMatch( child.getAlias() ) ) {
queryBuilder.append( " OPTIONAL MATCH " );
node( queryBuilder, targetAlias );
relationship( queryBuilder, child.getRelationshipName() );
node( queryBuilder, child.getAlias(), child.getTargetEntityName() );
}
appendOptionalMatch( queryBuilder, child.getAlias(), child.getChildren() );
}
}
private void where(StringBuilder queryBuilder, String targetAlias) {
StringBuilder whereCondition = builder.build();
if ( whereCondition != null ) {
queryBuilder.append( " WHERE " );
queryBuilder.append( whereCondition );
}
appendDiscriminatorClause( queryBuilder, targetAlias, whereCondition );
}
private void appendDiscriminatorClause(StringBuilder queryBuilder, String targetAlias, StringBuilder whereCondition) {
OgmEntityPersister entityPersister = getEntityPersister( targetType );
String discriminatorColumnName = entityPersister.getDiscriminatorColumnName();
if ( discriminatorColumnName != null ) {
// InheritanceType.SINGLE_TABLE
addConditionOnDiscriminatorValue( queryBuilder, targetAlias, whereCondition, entityPersister, discriminatorColumnName );
}
else if ( entityPersister.hasSubclasses() ) {
// InheritanceType.TABLE_PER_CLASS
@SuppressWarnings("unchecked")
Set<String> subclassEntityNames = entityPersister.getEntityMetamodel().getSubclassEntityNames();
throw LOG.queriesOnPolymorphicEntitiesAreNotSupportedWithTablePerClass( "Neo4j", subclassEntityNames );
}
}
private void addConditionOnDiscriminatorValue(StringBuilder queryBuilder, String targetAlias, StringBuilder whereCondition, OgmEntityPersister entityPersister,
String discriminatorColumnName) {
if ( whereCondition != null ) {
queryBuilder.append( " AND " );
}
else {
queryBuilder.append( " WHERE " );
}
@SuppressWarnings("unchecked")
Set<String> subclassEntityNames = entityPersister.getEntityMetamodel().getSubclassEntityNames();
identifier( queryBuilder, targetAlias );
queryBuilder.append( "." );
identifier( queryBuilder, discriminatorColumnName );
org.hibernate.type.Type discriminatorType = entityPersister.getDiscriminatorType();
if ( subclassEntityNames.size() == 1 ) {
queryBuilder.append( " = " );
appendDiscriminatorValue( queryBuilder, discriminatorType, entityPersister.getDiscriminatorValue() );
}
else {
queryBuilder.append( " IN [" );
Set<Object> discrimiantorValues = new HashSet<>();
discrimiantorValues.add( entityPersister.getDiscriminatorValue() );
String separator = "";
for ( String subclass : subclassEntityNames ) {
OgmEntityPersister subclassPersister = (OgmEntityPersister) sessionFactory.getEntityPersister( subclass );
Object discriminatorValue = subclassPersister.getDiscriminatorValue();
queryBuilder.append( separator );
appendDiscriminatorValue( queryBuilder, discriminatorType, discriminatorValue );
separator = ", ";
}
queryBuilder.append( "]" );
}
}
private void appendDiscriminatorValue(StringBuilder queryBuilder, org.hibernate.type.Type discriminatorType, Object discriminatorValue) {
Object value = convertToBackendType( discriminatorType, discriminatorValue );
CypherDSL.literal( queryBuilder, value );
}
private Object convertToBackendType(org.hibernate.type.Type discriminatorType, Object discriminatorValue) {
GridType ogmType = sessionFactory.getServiceRegistry().getService( TypeTranslator.class ).getType( discriminatorType );
return ogmType.convertToBackendType( discriminatorValue, sessionFactory );
}
private void orderBy(StringBuilder queryBuilder) {
if ( orderByExpressions != null && !orderByExpressions.isEmpty() ) {
queryBuilder.append( " ORDER BY " );
int counter = 1;
for ( OrderByClause orderBy : orderByExpressions ) {
orderBy.asString( queryBuilder );
if ( counter++ < orderByExpressions.size() ) {
queryBuilder.append( ", " );
}
}
}
}
private void returns(StringBuilder builder, String targetAlias) {
builder.append( " RETURN " );
if ( projections.isEmpty() ) {
identifier( builder, targetAlias );
}
else {
int counter = 1;
for ( String projection : projections ) {
builder.append( projection );
if ( counter++ < projections.size() ) {
builder.append( ", " );
}
}
}
}
@Override
public void setPropertyPath(PropertyPath path) {
if ( status == Status.DEFINING_SELECT ) {
defineSelect( path );
}
else {
this.propertyPath = path;
}
}
private void defineSelect(PropertyPath path) {
List<String> pathWithoutAlias = resolveAlias( path );
if ( !pathWithoutAlias.isEmpty() ) { // It might be empty if we have selected the target entity
// for the explicit joins, the relationships are already declared as required if needed so
// we will only declare new relationships for the implicit joins which are optional thus the requiredDepth
// set to 0
PropertyIdentifier identifier = propertyHelper.getPropertyIdentifier( targetTypeName, pathWithoutAlias, 0 );
String projection = identifier( identifier.getAlias(), identifier.getPropertyName() );
projections.add( projection );
}
}
@Override
protected void addSortField(PropertyPath propertyPath, String collateName, boolean isAscending) {
if ( orderByExpressions == null ) {
orderByExpressions = new ArrayList<OrderByClause>();
}
List<String> propertyPathWithoutAlias = resolveAlias( propertyPath );
PropertyIdentifier identifier = propertyHelper.getPropertyIdentifier( targetTypeName, propertyPathWithoutAlias, 0 );
OrderByClause order = new OrderByClause( identifier.getAlias(), identifier.getPropertyName(), isAscending );
orderByExpressions.add( order );
}
@Override
public void pushFromStrategy(JoinType joinType, Tree associationFetchTree, Tree propertyFetchTree, Tree alias) {
super.pushFromStrategy( joinType, associationFetchTree, propertyFetchTree, alias );
this.joinType = joinType;
}
@Override
public void popStrategy() {
super.popStrategy();
joinType = null;
}
@Override
public void registerJoinAlias(Tree alias, PropertyPath path) {
super.registerJoinAlias( alias, path );
List<String> propertyPath = resolveAlias( path );
int requiredDepth;
// For now, we deal with INNER JOIN and LEFT OUTER JOIN, it's not really perfect as you might have issues
// with join precedence but it's probably the best we can do for now.
if ( JoinType.INNER.equals( joinType ) ) {
requiredDepth = propertyPath.size();
}
else if ( JoinType.LEFT.equals( joinType ) ) {
requiredDepth = 0;
}
else {
LOG.joinTypeNotFullySupported( joinType );
// defaults to mark the alias as required for now
requiredDepth = propertyPath.size();
}
// Even if we don't need the property identifier, it's important to create the aliases for the corresponding
// associations/embedded with the correct requiredDepth.
propertyHelper.getPropertyIdentifier( targetTypeName, propertyPath, requiredDepth );
}
// TODO Methods below were not required here if fromNamedQuery() could be overridden from super
@Override
public void predicateLess(String comparativePredicate) {
addComparisonPredicate( comparativePredicate, Type.LESS );
}
@Override
public void predicateLessOrEqual(String comparativePredicate) {
addComparisonPredicate( comparativePredicate, Type.LESS_OR_EQUAL );
}
/**
* This implements the equality predicate; the comparison
* predicate could be a constant, a subfunction or
* some random type parameter.
* The tree node has all details but with current tree rendering
* it just passes it's text so we have to figure out the options again.
*/
@Override
public void predicateEquals(final String comparativePredicate) {
addComparisonPredicate( comparativePredicate, Type.EQUALS );
}
@Override
public void predicateNotEquals(String comparativePredicate) {
builder.pushNotPredicate();
addComparisonPredicate( comparativePredicate, Type.EQUALS );
builder.popBooleanPredicate();
}
@Override
public void predicateGreaterOrEqual(String comparativePredicate) {
addComparisonPredicate( comparativePredicate, Type.GREATER_OR_EQUAL );
}
@Override
public void predicateGreater(String comparativePredicate) {
addComparisonPredicate( comparativePredicate, Type.GREATER );
}
private void addComparisonPredicate(String comparativePredicate, Type comparisonType) {
Object comparisonValue = fromNamedQuery( comparativePredicate );
List<String> property = resolveAlias( propertyPath );
builder.addComparisonPredicate( property, comparisonType, comparisonValue );
}
@Override
public void predicateIn(List<String> list) {
List<Object> values = fromNamedQuery( list );
List<String> property = resolveAlias( propertyPath );
builder.addInPredicate( property, values );
}
@Override
public void predicateBetween(String lower, String upper) {
Object lowerComparisonValue = fromNamedQuery( lower );
Object upperComparisonValue = fromNamedQuery( upper );
List<String> property = resolveAlias( propertyPath );
builder.addRangePredicate( property, lowerComparisonValue, upperComparisonValue );
}
@Override
public void predicateLike(String patternValue, Character escapeCharacter) {
Object pattern = fromNamedQuery( patternValue );
List<String> property = resolveAlias( propertyPath );
builder.addLikePredicate( property, (String) pattern, escapeCharacter );
}
@Override
public void predicateIsNull() {
List<String> property = resolveAlias( propertyPath );
builder.addIsNullPredicate( property );
}
private Object fromNamedQuery(String comparativePredicate) {
// It's a named parameter; Value given via setParameter(), taking that as is
if ( comparativePredicate.startsWith( ":" ) ) {
return new Neo4jQueryParameter( comparativePredicate.substring( 1 ) );
}
// It's a value given in JP-QL; Convert the literal value
else {
List<String> path = new ArrayList<String>();
path.addAll( propertyPath.getNodeNamesWithoutAlias() );
PropertyPath fullPath = propertyPath;
// create the complete path in case it's a join
while ( fullPath.getFirstNode().isAlias() && aliasToPropertyPath.containsKey( fullPath.getFirstNode().getName() ) ) {
fullPath = aliasToPropertyPath.get( fullPath.getFirstNode().getName() );
path.addAll( 0, fullPath.getNodeNamesWithoutAlias() );
}
return propertyHelper.convertToPropertyType( targetTypeName, path, comparativePredicate );
}
}
private List<Object> fromNamedQuery(List<String> list) {
List<Object> elements = new ArrayList<Object>( list.size() );
for ( String string : list ) {
elements.add( fromNamedQuery( string ) );
}
return elements;
}
}