Open Source Repository

Home /hibernate/hibernate-3.2.4.ga | Repository Home



org/hibernate/hql/ast/tree/DotNode.java
// $Id: DotNode.java 11379 2007-03-30 11:23:58Z [email protected] $
package org.hibernate.hql.ast.tree;

import org.hibernate.QueryException;
import org.hibernate.engine.JoinSequence;
import org.hibernate.hql.CollectionProperties;
import org.hibernate.hql.antlr.SqlTokenTypes;
import org.hibernate.hql.ast.util.ASTPrinter;
import org.hibernate.hql.ast.util.ASTUtil;
import org.hibernate.hql.ast.util.ColumnHelper;
import org.hibernate.persister.collection.QueryableCollection;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.sql.JoinFragment;
import org.hibernate.type.CollectionType;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;
import org.hibernate.util.StringHelper;

import antlr.SemanticException;
import antlr.collections.AST;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Represents a reference to a property or alias expression.  This should duplicate the relevant behaviors in
 * PathExpressionParser.
 *
 @author Joshua Davis
 */
public class DotNode extends FromReferenceNode implements DisplayableNode, SelectExpression {

  ///////////////////////////////////////////////////////////////////////////
  // USED ONLY FOR REGRESSION TESTING!!!!
  // 
  // todo : obviously get rid of all this junk ;)
  ///////////////////////////////////////////////////////////////////////////
  public static boolean useThetaStyleImplicitJoins = false;
  public static boolean REGRESSION_STYLE_JOIN_SUPPRESSION = false;
  public static interface IllegalCollectionDereferenceExceptionBuilder {
    public QueryException buildIllegalCollectionDereferenceException(String collectionPropertyName, FromReferenceNode lhs);
  }
  public static final IllegalCollectionDereferenceExceptionBuilder DEF_ILLEGAL_COLL_DEREF_EXCP_BUILDER = new IllegalCollectionDereferenceExceptionBuilder() {
    public QueryException buildIllegalCollectionDereferenceException(String propertyName, FromReferenceNode lhs) {
      String lhsPath = ASTUtil.getPathTextlhs );
      return new QueryException"illegal attempt to dereference collection [" + lhsPath + "] with element property reference [" + propertyName + "]" );
    }
  };
  public static IllegalCollectionDereferenceExceptionBuilder ILLEGAL_COLL_DEREF_EXCP_BUILDER = DEF_ILLEGAL_COLL_DEREF_EXCP_BUILDER;
  ///////////////////////////////////////////////////////////////////////////

  private static final Log log = LogFactory.getLogDotNode.class );

  private static final int DEREF_UNKNOWN = 0;
  private static final int DEREF_ENTITY = 1;
  private static final int DEREF_COMPONENT = 2;
  private static final int DEREF_COLLECTION = 3;
  private static final int DEREF_PRIMITIVE = 4;
  private static final int DEREF_IDENTIFIER = 5;
  private static final int DEREF_JAVA_CONSTANT = 6;

  /**
   * The identifier that is the name of the property.
   */
  private String propertyName;
  /**
   * The full path, to the root alias of this dot node.
   */
  private String path;
  /**
   * The unresolved property path relative to this dot node.
   */
  private String propertyPath;

  /**
   * The column names that this resolves to.
   */
  private String[] columns;

  /**
   * The type of join to create.   Default is an inner join.
   */
  private int joinType = JoinFragment.INNER_JOIN;

  /**
   * Fetch join or not.
   */
  private boolean fetch = false;

  /**
   * The type of dereference that hapened (DEREF_xxx).
   */
  private int dereferenceType = DEREF_UNKNOWN;

  private FromElement impliedJoin;

  /**
   * Sets the join type for this '.' node structure.
   *
   @param joinType The type of join to use.
   @see JoinFragment
   */
  public void setJoinType(int joinType) {
    this.joinType = joinType;
  }

  private String[] getColumns() throws QueryException {
    if columns == null ) {
      // Use the table fromElement and the property name to get the array of column names.
      String tableAlias = getLhs().getFromElement().getTableAlias();
      columns = getFromElement().toColumnstableAlias, propertyPath, false );
    }
    return columns;
  }

  public String getDisplayText() {
    StringBuffer buf = new StringBuffer();
    FromElement fromElement = getFromElement();
    buf.append"{propertyName=" ).appendpropertyName );
    buf.append",dereferenceType=" ).appendASTPrinter.getConstantNamegetClass(), dereferenceType ) );
    buf.append",propertyPath=" ).appendpropertyPath );
    buf.append",path=" ).appendgetPath() );
    if fromElement != null ) {
      buf.append",tableAlias=" ).appendfromElement.getTableAlias() );
      buf.append",className=" ).appendfromElement.getClassName() );
      buf.append",classAlias=" ).appendfromElement.getClassAlias() );
    }
    else {
      buf.append",no from element" );
    }
    buf.append'}' );
    return buf.toString();
  }

  /**
   * Resolves the left hand side of the DOT.
   *
   @throws SemanticException
   */
  public void resolveFirstChild() throws SemanticException {
    FromReferenceNode lhs = FromReferenceNode getFirstChild();
    SqlNode property = SqlNode lhs.getNextSibling();

    // Set the attributes of the property reference expression.
    String propName = property.getText();
    propertyName = propName;
    // If the uresolved property path isn't set yet, just use the property name.
    if propertyPath == null ) {
      propertyPath = propName;
    }
    // Resolve the LHS fully, generate implicit joins.  Pass in the property name so that the resolver can
    // discover foreign key (id) properties.
    lhs.resolvetrue, true, null, this );
    setFromElementlhs.getFromElement() );      // The 'from element' that the property is in.

    checkSubclassOrSuperclassPropertyReferencelhs, propName );
  }
  
  public void resolveInFunctionCall(boolean generateJoin, boolean implicitJointhrows SemanticException {
    if isResolved() ) {
      return;
    }
    Type propertyType = prepareLhs();      // Prepare the left hand side and get the data type.
    if propertyType!=null && propertyType.isCollectionType() ) {
      resolveIndex(null);
    }
    else {
      resolveFirstChild();
      super.resolve(generateJoin, implicitJoin);
    }
  }


  public void resolveIndex(AST parentthrows SemanticException {
    if isResolved() ) {
      return;
    }
    Type propertyType = prepareLhs();      // Prepare the left hand side and get the data type.
    dereferenceCollection( ( CollectionType propertyType, true, true, null, parent );
  }

  public void resolve(boolean generateJoin, boolean implicitJoin, String classAlias, AST parent
  throws SemanticException {
    // If this dot has already been resolved, stop now.
    if isResolved() ) {
      return;
    }
    Type propertyType = prepareLhs()// Prepare the left hand side and get the data type.

    // If there is no data type for this node, and we're at the end of the path (top most dot node), then
    // this might be a Java constant.
    if propertyType == null ) {
      if parent == null ) {
        getWalker().getLiteralProcessor().lookupConstantthis );
      }
      // If the propertyType is null and there isn't a parent, just 
      // stop now... there was a problem resolving the node anyway.
      return;
    }

    if propertyType.isComponentType() ) {
      // The property is a component...
      checkLhsIsNotCollection();
      dereferenceComponentparent );
      initText();
    }
    else if propertyType.isEntityType() ) {
      // The property is another class..
      checkLhsIsNotCollection();
      dereferenceEntity( ( EntityType propertyType, implicitJoin, classAlias, generateJoin, parent );
      initText();
    }
    else if propertyType.isCollectionType() ) {
      // The property is a collection...
      checkLhsIsNotCollection();
      dereferenceCollection( ( CollectionType propertyType, implicitJoin, false, classAlias, parent );
    }
    else {
      // Otherwise, this is a primitive type.
      if ! CollectionProperties.isAnyCollectionPropertypropertyName ) ) {
        checkLhsIsNotCollection();
      }
      dereferenceType = DEREF_PRIMITIVE;
      initText();
    }
    setResolved();
  }
  
  private void initText() {
    String[] cols = getColumns();
    String text = StringHelper.join", ", cols );
    if cols.length > && getWalker().isComparativeExpressionClause() ) {
      text = "(" + text + ")";
    }
    setTexttext );
  }

  private Type prepareLhs() throws SemanticException {
    FromReferenceNode lhs = getLhs();
    lhs.prepareForDotpropertyName );
    return getDataType();
  }

  private void dereferenceCollection(CollectionType collectionType, boolean implicitJoin, boolean indexed, String classAlias, AST parent)
  throws SemanticException {
    
    dereferenceType = DEREF_COLLECTION;
    String role = collectionType.getRole();
    
    //foo.bars.size (also handles deprecated stuff like foo.bars.maxelement for backwardness)
    boolean isSizeProperty = getNextSibling()!=null && 
      CollectionProperties.isAnyCollectionPropertygetNextSibling().getText() );

    if isSizeProperty indexed = true//yuck!

    QueryableCollection queryableCollection = getSessionFactoryHelper().requireQueryableCollectionrole );
    String propName = getPath();
    FromClause currentFromClause = getWalker().getCurrentFromClause();

    if getWalker().getStatementType() != SqlTokenTypes.SELECT && indexed && classAlias == null ) {
      // should indicate that we are processing an INSERT/UPDATE/DELETE
      // query with a subquery implied via a collection property
      // function. Here, we need to use the table name itself as the
      // qualification alias.
      // TODO : verify this works for all databases...
      // TODO : is this also the case in non-"indexed" scenarios?
      String alias = getLhs().getFromElement().getQueryable().getTableName();
      columns = getFromElement().toColumnsalias, propertyPath, false, true );
    }

    //We do not look for an existing join on the same path, because
    //it makes sense to join twice on the same collection role
    FromElementFactory factory = new FromElementFactory(
            currentFromClause,
            getLhs().getFromElement(),
            propName,
        classAlias,
            getColumns(),
            implicitJoin
    );
    FromElement elem = factory.createCollectionqueryableCollection, role, joinType, fetch, indexed );
    
    if log.isDebugEnabled() ) {
      log.debug"dereferenceCollection() : Created new FROM element for " + propName + " : " + elem );
    }
    
    setImpliedJoinelem );
    setFromElementelem );  // This 'dot' expression now refers to the resulting from element.
    
    if isSizeProperty ) {
      elem.setText("");
      elem.setUseWhereFragment(false);
    }
    
    if !implicitJoin ) {
      EntityPersister entityPersister = elem.getEntityPersister();
      if entityPersister != null ) {
        getWalker().addQuerySpacesentityPersister.getQuerySpaces() );
      }
    }
    getWalker().addQuerySpacesqueryableCollection.getCollectionSpaces() );  // Always add the collection's query spaces.
  }

  private void dereferenceEntity(EntityType entityType, boolean implicitJoin, String classAlias, boolean generateJoin, AST parentthrows SemanticException {
    checkForCorrelatedSubquery"dereferenceEntity" );
    // three general cases we check here as to whether to render a physical SQL join:
    // 1) is our parent a DotNode as well?  If so, our property reference is
    //     being further de-referenced...
    // 2) is this a DML statement
    // 3) we were asked to generate any needed joins (generateJoins==true) *OR*
    //    we are currently processing a select or from clause
    // (an additional check is the REGRESSION_STYLE_JOIN_SUPPRESSION check solely intended for the test suite)
    //
    // The REGRESSION_STYLE_JOIN_SUPPRESSION is an additional check
    // intended solely for use within the test suite.  This forces the
    // implicit join resolution to behave more like the classic parser.
    // The underlying issue is that classic translator is simply wrong
    // about its decisions on whether or not to render an implicit join
    // into a physical SQL join in a lot of cases.  The piece it generally
    // tends to miss is that INNER joins effect the results by further
    // restricting the data set!  A particular manifestation of this is
    // the fact that the classic translator will skip the physical join
    // for ToOne implicit joins *if the query is shallow*; the result
    // being that Query.list() and Query.iterate() could return
    // different number of results!
    DotNode parentAsDotNode = null;
    String property = propertyName;
    final boolean joinIsNeeded;

    if isDotNodeparent ) ) {
      // our parent is another dot node, meaning we are being further dereferenced.
      // thus we need to generate a join unless the parent refers to the associated
      // entity's PK (because 'our' table would know the FK).
      parentAsDotNode = DotNode parent;
      property = parentAsDotNode.propertyName;
      joinIsNeeded = generateJoin && !isReferenceToPrimaryKeyparentAsDotNode.propertyName, entityType );
    }
    else if ! getWalker().isSelectStatement() ) {
      joinIsNeeded = false;
    }
    else if REGRESSION_STYLE_JOIN_SUPPRESSION ) {
      // this is the regression style determination which matches the logic of the classic translator
      joinIsNeeded = generateJoin && !getWalker().isInSelect() || !getWalker().isShallowQuery() );
    }
    else {
      joinIsNeeded = generateJoin || getWalker().isInSelect() || getWalker().isInFrom() );
    }

    if joinIsNeeded ) {
      dereferenceEntityJoinclassAlias, entityType, implicitJoin, parent );
    }
    else {
      dereferenceEntityIdentifierproperty, parentAsDotNode );
    }

  }

  private boolean isDotNode(AST n) {
    return n != null && n.getType() == SqlTokenTypes.DOT;
  }

  private void dereferenceEntityJoin(String classAlias, EntityType propertyType, boolean impliedJoin, AST parent
  throws SemanticException {
    dereferenceType = DEREF_ENTITY;
    if log.isDebugEnabled() ) {
      log.debug"dereferenceEntityJoin() : generating join for " + propertyName + " in "
          + getFromElement().getClassName() " "
          ( ( classAlias == null "{no alias}" "(" + classAlias + ")" )
          " parent = " + ASTUtil.getDebugStringparent )
      );
    }
    // Create a new FROM node for the referenced class.
    String associatedEntityName = propertyType.getAssociatedEntityName();
    String tableAlias = getAliasGenerator().createNameassociatedEntityName );

    String[] joinColumns = getColumns();
    String joinPath = getPath();

    if impliedJoin && getWalker().isInFrom() ) {
      joinType = getWalker().getImpliedJoinType();
    }

    FromClause currentFromClause = getWalker().getCurrentFromClause();
    FromElement elem = currentFromClause.findJoinByPathjoinPath );

///////////////////////////////////////////////////////////////////////////////
//
// This is the piece which recognizes the condition where an implicit join path
// resolved earlier in a correlated subquery is now being referenced in the
// outer query.  For 3.0final, we just let this generate a second join (which
// is exactly how the old parser handles this).  Eventually we need to add this
// logic back in and complete the logic in FromClause.promoteJoin; however,
// FromClause.promoteJoin has its own difficulties (see the comments in
// FromClause.promoteJoin).
//
//    if ( elem == null ) {
//      // see if this joinPath has been used in a "child" FromClause, and if so
//      // promote that element to the outer query
//      FromClause currentNodeOwner = getFromElement().getFromClause();
//      FromClause currentJoinOwner = currentNodeOwner.locateChildFromClauseWithJoinByPath( joinPath );
//      if ( currentJoinOwner != null && currentNodeOwner != currentJoinOwner ) {
//        elem = currentJoinOwner.findJoinByPathLocal( joinPath );
//        if ( elem != null ) {
//          currentFromClause.promoteJoin( elem );
//          // EARLY EXIT!!!
//          return;
//        }
//      }
//    }
//
///////////////////////////////////////////////////////////////////////////////

    if elem == null ) {
      // If this is an implied join in a from element, then use the impled join type which is part of the
      // tree parser's state (set by the gramamar actions).
      JoinSequence joinSequence = getSessionFactoryHelper()
        .createJoinSequenceimpliedJoin, propertyType, tableAlias, joinType, joinColumns );

      FromElementFactory factory = new FromElementFactory(
              currentFromClause,
          getLhs().getFromElement(),
          joinPath, 
          classAlias, 
          joinColumns, 
          impliedJoin
      );
      elem = factory.createEntityJoin
          associatedEntityName, 
          tableAlias, 
          joinSequence, 
          fetch, 
          getWalker().isInFrom()
          propertyType
      );
    }
    else {
      currentFromClause.addDuplicateAlias(classAlias, elem);
    }
    setImpliedJoinelem );
    getWalker().addQuerySpaceselem.getEntityPersister().getQuerySpaces() );
    setFromElementelem );  // This 'dot' expression now refers to the resulting from element.
  }

  private void setImpliedJoin(FromElement elem) {
    this.impliedJoin = elem;
    if getFirstChild().getType() == SqlTokenTypes.DOT ) {
      DotNode dotLhs = DotNode getFirstChild();
      if dotLhs.getImpliedJoin() != null ) {
        this.impliedJoin = dotLhs.getImpliedJoin();
      }
    }
  }

  public FromElement getImpliedJoin() {
    return impliedJoin;
  }

  /**
   * Is the given property name a reference to the primary key of the associated
   * entity construed by the given entity type?
   <p/>
   * For example, consider a fragment like order.customer.id
   * (where order is a from-element alias).  Here, we'd have:
   * propertyName = "id" AND
   * owningType = ManyToOneType(Customer)
   * and are being asked to determine whether "customer.id" is a reference
   * to customer's PK...
   *
   @param propertyName The name of the property to check.
   @param owningType The type represeting the entity "owning" the property
   @return True if propertyName references the entity's (owningType->associatedEntity)
   * primary key; false otherwise.
   */
  private boolean isReferenceToPrimaryKey(String propertyName, EntityType owningType) {
    EntityPersister persister = getSessionFactoryHelper()
        .getFactory()
        .getEntityPersisterowningType.getAssociatedEntityName() );
    if persister.getEntityMetamodel().hasNonIdentifierPropertyNamedId() ) {
      // only the identifier property field name can be a reference to the associated entity's PK...
      return propertyName.equalspersister.getIdentifierPropertyName() ) && owningType.isReferenceToPrimaryKey();
    }
    else {
      // here, we have two possibilities:
      //     1) the property-name matches the explicitly identifier property name
      //    2) the property-name matches the implicit 'id' property name
      if EntityPersister.ENTITY_ID.equalspropertyName ) ) {
        // the referenced node text is the special 'id'
        return owningType.isReferenceToPrimaryKey();
      }
      else {
        String keyPropertyName = getSessionFactoryHelper().getIdentifierOrUniqueKeyPropertyNameowningType );
        return keyPropertyName != null && keyPropertyName.equalspropertyName && owningType.isReferenceToPrimaryKey();
      }
    }
  }

  private void checkForCorrelatedSubquery(String methodName) {
    if isCorrelatedSubselect() ) {
      if log.isDebugEnabled() ) {
        log.debugmethodName + "() : correlated subquery" );
      }
    }
  }

  private boolean isCorrelatedSubselect() {
    return getWalker().isSubQuery() &&
      getFromElement().getFromClause() != getWalker().getCurrentFromClause();
  }

  private void checkLhsIsNotCollection() throws SemanticException {
    if getLhs().getDataType() != null && getLhs().getDataType().isCollectionType() ) {
      throw ILLEGAL_COLL_DEREF_EXCP_BUILDER.buildIllegalCollectionDereferenceExceptionpropertyName, getLhs() );
    }
  }

  private void dereferenceComponent(AST parent) {
    dereferenceType = DEREF_COMPONENT;
    setPropertyNameAndPathparent );
  }

  private void dereferenceEntityIdentifier(String propertyName, DotNode dotParent) {
    // special shortcut for id properties, skip the join!
    // this must only occur at the _end_ of a path expression
    if log.isDebugEnabled() ) {
      log.debug"dereferenceShortcut() : property " 
        propertyName + " in " + getFromElement().getClassName() 
        " does not require a join." );
    }

    initText();
    setPropertyNameAndPathdotParent )// Set the unresolved path in this node and the parent.
    // Set the text for the parent.
    if dotParent != null ) {
      dotParent.dereferenceType = DEREF_IDENTIFIER;
      dotParent.setTextgetText() );
      dotParent.columns = getColumns();
    }
  }

  private void setPropertyNameAndPath(AST parent) {
    if isDotNodeparent ) ) {
      DotNode dotNode = DotNode parent;
      AST lhs = dotNode.getFirstChild();
      AST rhs = lhs.getNextSibling();
      propertyName = rhs.getText();
      propertyPath = propertyPath + "." + propertyName; // Append the new property name onto the unresolved path.
      dotNode.propertyPath = propertyPath;
      if log.isDebugEnabled() ) {
        log.debug"Unresolved property path is now '" + dotNode.propertyPath + "'" );
      }
    }
    else {
      if log.isDebugEnabled() ) {
        log.debug"terminal propertyPath = [" + propertyPath + "]" );
      }
    }
  }

  public Type getDataType() {
    if super.getDataType() == null ) {
      FromElement fromElement = getLhs().getFromElement();
      if fromElement == null ) {
        return null;
      }
      // If the lhs is a collection, use CollectionPropertyMapping
      Type propertyType = fromElement.getPropertyTypepropertyName, propertyPath );
      if log.isDebugEnabled() ) {
        log.debug"getDataType() : " + propertyPath + " -> " + propertyType );
      }
      super.setDataTypepropertyType );
    }
    return super.getDataType();
  }

  public void setPropertyPath(String propertyPath) {
    this.propertyPath = propertyPath;
  }

  public String getPropertyPath() {
    return propertyPath;
  }

  public FromReferenceNode getLhs() {
    FromReferenceNode lhs = ( ( FromReferenceNode getFirstChild() );
    if lhs == null ) {
      throw new IllegalStateException"DOT node with no left-hand-side!" );
    }
    return lhs;
  }

  /**
   * Returns the full path of the node.
   *
   @return the full path of the node.
   */
  public String getPath() {
    if path == null ) {
      FromReferenceNode lhs = getLhs();
      if lhs == null ) {
        path = getText();
      }
      else {
        SqlNode rhs = SqlNode lhs.getNextSibling();
        path = lhs.getPath() "." + rhs.getOriginalText();
      }
    }
    return path;
  }

  public void setFetch(boolean fetch) {
    this.fetch = fetch;
  }

  public void setScalarColumnText(int ithrows SemanticException {
    String[] sqlColumns = getColumns();
    ColumnHelper.generateScalarColumnsthis, sqlColumns, i );
  }

  /**
   * Special method to resolve expressions in the SELECT list.
   *
   @throws SemanticException if this cannot be resolved.
   */
  public void resolveSelectExpression() throws SemanticException {
    if getWalker().isShallowQuery() || getWalker().getCurrentFromClause().isSubQuery() ) {
      resolve(false, true);
    }
    else {
      resolve(true, false);
      Type type = getDataType();
      if type.isEntityType() ) {
        FromElement fromElement = getFromElement();
        fromElement.setIncludeSubclassestrue )// Tell the destination fromElement to 'includeSubclasses'.
        if useThetaStyleImplicitJoins ) {
          fromElement.getJoinSequence().setUseThetaStyletrue );  // Use theta style (for regression)
          // Move the node up, after the origin node.
          FromElement origin = fromElement.getOrigin();
          if origin != null ) {
            ASTUtil.makeSiblingOfParentorigin, fromElement );
          }
        }
      }
    }

    FromReferenceNode lhs = getLhs();
    while lhs != null ) {
      checkSubclassOrSuperclassPropertyReferencelhs, lhs.getNextSibling().getText() );
      lhs = FromReferenceNode lhs.getFirstChild();
    }
  }

  public void setResolvedConstant(String text) {
    path = text;
    dereferenceType = DEREF_JAVA_CONSTANT;
    setResolved()// Don't resolve the node again.
  }

  private boolean checkSubclassOrSuperclassPropertyReference(FromReferenceNode lhs, String propertyName) {
    if lhs != null && !lhs instanceof IndexNode ) ) {
      final FromElement source = lhs.getFromElement();
      if source != null ) {
        source.handlePropertyBeingDereferencedlhs.getDataType(), propertyName );
      }
    }
    return false;
  }
}