/* * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat Middleware LLC. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA * */ package org.hibernate.hql.internal.ast.tree; import java.util.List; import java.util.Map; import org.hibernate.engine.internal.JoinSequence; import org.hibernate.hql.internal.CollectionProperties; import org.hibernate.hql.internal.CollectionSubqueryFactory; import org.hibernate.hql.internal.NameGenerator; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.MappingException; import org.hibernate.QueryException; import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.param.ParameterSpecification; import org.hibernate.persister.collection.CollectionPropertyMapping; import org.hibernate.persister.collection.CollectionPropertyNames; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.Joinable; import org.hibernate.persister.entity.PropertyMapping; import org.hibernate.persister.entity.Queryable; import org.hibernate.type.EntityType; import org.hibernate.type.Type; import org.jboss.logging.Logger; /** * Delegate that handles the type and join sequence information for a FromElement. * * @author josh */ class FromElementType { private static final CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class, FromElementType.class.getName()); private FromElement fromElement; private EntityType entityType; private EntityPersister persister; private QueryableCollection queryableCollection; private CollectionPropertyMapping collectionPropertyMapping; private JoinSequence joinSequence; private String collectionSuffix; private ParameterSpecification indexCollectionSelectorParamSpec; public FromElementType(FromElement fromElement, EntityPersister persister, EntityType entityType) { this.fromElement = fromElement; this.persister = persister; this.entityType = entityType; if ( persister != null ) { fromElement.setText( ( ( Queryable ) persister ).getTableName() + " " + getTableAlias() ); } } protected FromElementType(FromElement fromElement) { this.fromElement = fromElement; } private String getTableAlias() { return fromElement.getTableAlias(); } private String getCollectionTableAlias() { return fromElement.getCollectionTableAlias(); } public String getCollectionSuffix() { return collectionSuffix; } public void setCollectionSuffix(String suffix) { collectionSuffix = suffix; } public EntityPersister getEntityPersister() { return persister; } public Type getDataType() { if ( persister == null ) { if ( queryableCollection == null ) { return null; } return queryableCollection.getType(); } else { return entityType; } } public Type getSelectType() { if (entityType==null) return null; boolean shallow = fromElement.getFromClause().getWalker().isShallowQuery(); return fromElement.getSessionFactoryHelper() .getFactory() .getTypeResolver() .getTypeFactory().manyToOne( entityType.getAssociatedEntityName(), shallow ); } /** * Returns the Hibernate queryable implementation for the HQL class. * * @return the Hibernate queryable implementation for the HQL class. */ public Queryable getQueryable() { return ( persister instanceof Queryable ) ? ( Queryable ) persister : null; } /** * Render the identifier select, but in a 'scalar' context (i.e. generate the column alias). * * @param i the sequence of the returned type * @return the identifier select with the column alias. */ String renderScalarIdentifierSelect(int i) { checkInitialized(); String[] cols = getPropertyMapping( EntityPersister.ENTITY_ID ).toColumns( getTableAlias(), EntityPersister.ENTITY_ID ); StringBuffer buf = new StringBuffer(); // For property references generate <tablealias>.<columnname> as <projectionalias> for ( int j = 0; j < cols.length; j++ ) { String column = cols[j]; if ( j > 0 ) { buf.append( ", " ); } buf.append( column ).append( " as " ).append( NameGenerator.scalarName( i, j ) ); } return buf.toString(); } /** * Returns the identifier select SQL fragment. * * @param size The total number of returned types. * @param k The sequence of the current returned type. * @return the identifier select SQL fragment. */ String renderIdentifierSelect(int size, int k) { checkInitialized(); // Render the identifier select fragment using the table alias. if ( fromElement.getFromClause().isSubQuery() ) { // TODO: Replace this with a more elegant solution. String[] idColumnNames = ( persister != null ) ? ( ( Queryable ) persister ).getIdentifierColumnNames() : new String[0]; StringBuffer buf = new StringBuffer(); for ( int i = 0; i < idColumnNames.length; i++ ) { buf.append( fromElement.getTableAlias() ).append( '.' ).append( idColumnNames[i] ); if ( i != idColumnNames.length - 1 ) buf.append( ", " ); } return buf.toString(); } else { if (persister==null) { throw new QueryException( "not an entity" ); } String fragment = ( ( Queryable ) persister ).identifierSelectFragment( getTableAlias(), getSuffix( size, k ) ); return trimLeadingCommaAndSpaces( fragment ); } } private String getSuffix(int size, int sequence) { return generateSuffix( size, sequence ); } private static String generateSuffix(int size, int k) { String suffix = size == 1 ? "" : Integer.toString( k ) + '_'; return suffix; } private void checkInitialized() { fromElement.checkInitialized(); } /** * Returns the property select SQL fragment. * @param size The total number of returned types. * @param k The sequence of the current returned type. * @return the property select SQL fragment. */ String renderPropertySelect(int size, int k, boolean allProperties) { checkInitialized(); if ( persister == null ) { return ""; } else { String fragment = ( ( Queryable ) persister ).propertySelectFragment( getTableAlias(), getSuffix( size, k ), allProperties ); return trimLeadingCommaAndSpaces( fragment ); } } String renderCollectionSelectFragment(int size, int k) { if ( queryableCollection == null ) { return ""; } else { if ( collectionSuffix == null ) { collectionSuffix = generateSuffix( size, k ); } String fragment = queryableCollection.selectFragment( getCollectionTableAlias(), collectionSuffix ); return trimLeadingCommaAndSpaces( fragment ); } } public String renderValueCollectionSelectFragment(int size, int k) { if ( queryableCollection == null ) { return ""; } else { if ( collectionSuffix == null ) { collectionSuffix = generateSuffix( size, k ); } String fragment = queryableCollection.selectFragment( getTableAlias(), collectionSuffix ); return trimLeadingCommaAndSpaces( fragment ); } } /** * This accounts for a quirk in Queryable, where it sometimes generates ', ' in front of the * SQL fragment. :-P * * @param fragment An SQL fragment. * @return The fragment, without the leading comma and spaces. */ private static String trimLeadingCommaAndSpaces(String fragment) { if ( fragment.length() > 0 && fragment.charAt( 0 ) == ',' ) { fragment = fragment.substring( 1 ); } fragment = fragment.trim(); return fragment.trim(); } public void setJoinSequence(JoinSequence joinSequence) { this.joinSequence = joinSequence; } public JoinSequence getJoinSequence() { if ( joinSequence != null ) { return joinSequence; } // Class names in the FROM clause result in a JoinSequence (the old FromParser does this). if ( persister instanceof Joinable ) { Joinable joinable = ( Joinable ) persister; return fromElement.getSessionFactoryHelper().createJoinSequence().setRoot( joinable, getTableAlias() ); } else { return null; // TODO: Should this really return null? If not, figure out something better to do here. } } public void setQueryableCollection(QueryableCollection queryableCollection) { if ( this.queryableCollection != null ) { throw new IllegalStateException( "QueryableCollection is already defined for " + this + "!" ); } this.queryableCollection = queryableCollection; if ( !queryableCollection.isOneToMany() ) { // For many-to-many joins, use the tablename from the queryable collection for the default text. fromElement.setText( queryableCollection.getTableName() + " " + getTableAlias() ); } } public QueryableCollection getQueryableCollection() { return queryableCollection; } /** * Returns the type of a property, given it's name (the last part) and the full path. * * @param propertyName The last part of the full path to the property. * @return The type. * @0param propertyPath The full property path. */ public Type getPropertyType(String propertyName, String propertyPath) { checkInitialized(); Type type = null; // If this is an entity and the property is the identifier property, then use getIdentifierType(). // Note that the propertyName.equals( propertyPath ) checks whether we have a component // key reference, where the component class property name is the same as the // entity id property name; if the two are not equal, this is the case and // we'd need to "fall through" to using the property mapping. if ( persister != null && propertyName.equals( propertyPath ) && propertyName.equals( persister.getIdentifierPropertyName() ) ) { type = persister.getIdentifierType(); } else { // Otherwise, use the property mapping. PropertyMapping mapping = getPropertyMapping( propertyName ); type = mapping.toType( propertyPath ); } if ( type == null ) { throw new MappingException( "Property " + propertyName + " does not exist in " + ( ( queryableCollection == null ) ? "class" : "collection" ) + " " + ( ( queryableCollection == null ) ? fromElement.getClassName() : queryableCollection.getRole() ) ); } return type; } String[] toColumns(String tableAlias, String path, boolean inSelect) { return toColumns( tableAlias, path, inSelect, false ); } String[] toColumns(String tableAlias, String path, boolean inSelect, boolean forceAlias) { checkInitialized(); PropertyMapping propertyMapping = getPropertyMapping( path ); // If this from element is a collection and the path is a collection property (maxIndex, etc.) then // generate a sub-query. // // NOTE : in the case of this being a collection property in the select, not generating the subquery // will not generally work. The specific cases I am thinking about are the minIndex, maxIndex // (most likely minElement, maxElement as well) cases. // todo : if ^^ is the case we should thrown an exception here rather than waiting for the sql error // if the dialect supports select-clause subqueries we could go ahead and generate the subquery also if ( !inSelect && queryableCollection != null && CollectionProperties.isCollectionProperty( path ) ) { Map enabledFilters = fromElement.getWalker().getEnabledFilters(); String subquery = CollectionSubqueryFactory.createCollectionSubquery( joinSequence.copy().setUseThetaStyle( true ), enabledFilters, propertyMapping.toColumns( tableAlias, path ) ); LOG.debugf("toColumns(%s,%s) : subquery = %s", tableAlias, path, subquery); return new String[]{"(" + subquery + ")"}; } if (forceAlias) { return propertyMapping.toColumns(tableAlias, path); } else if (fromElement.getWalker().getStatementType() == HqlSqlTokenTypes.SELECT) { return propertyMapping.toColumns(tableAlias, path); } else if (fromElement.getWalker().getCurrentClauseType() == HqlSqlTokenTypes.SELECT) { return propertyMapping.toColumns(tableAlias, path); } else if (fromElement.getWalker().isSubQuery()) { // for a subquery, the alias to use depends on a few things (we // already know this is not an overall SELECT): // 1) if this FROM_ELEMENT represents a correlation to the // outer-most query // A) if the outer query represents a multi-table // persister, we need to use the given alias // in anticipation of one of the multi-table // executors being used (as this subquery will // actually be used in the "id select" phase // of that multi-table executor) // B) otherwise, we need to use the persister's // table name as the column qualification // 2) otherwise (not correlated), use the given alias if (isCorrelation()) { if (isMultiTable()) return propertyMapping.toColumns(tableAlias, path); return propertyMapping.toColumns(extractTableName(), path); } return propertyMapping.toColumns(tableAlias, path); } else { String[] columns = propertyMapping.toColumns(path); LOG.trace("Using non-qualified column reference [" + path + " -> (" + ArrayHelper.toString(columns) + ")]"); return columns; } } private boolean isCorrelation() { FromClause top = fromElement.getWalker().getFinalFromClause(); return fromElement.getFromClause() != fromElement.getWalker().getCurrentFromClause() && fromElement.getFromClause() == top; } private boolean isMultiTable() { // should be safe to only ever expect EntityPersister references here return fromElement.getQueryable() != null && fromElement.getQueryable().isMultiTable(); } private String extractTableName() { // should be safe to only ever expect EntityPersister references here return fromElement.getQueryable().getTableName(); } private static final List SPECIAL_MANY2MANY_TREATMENT_FUNCTION_NAMES = java.util.Arrays.asList( new String[] { CollectionPropertyNames.COLLECTION_INDEX, CollectionPropertyNames.COLLECTION_MIN_INDEX, CollectionPropertyNames.COLLECTION_MAX_INDEX } ); PropertyMapping getPropertyMapping(String propertyName) { checkInitialized(); if ( queryableCollection == null ) { // Not a collection? return ( PropertyMapping ) persister; // Return the entity property mapping. } // indexed, many-to-many collections must be treated specially here if the property to // be mapped touches on the index as we must adjust the alias to use the alias from // the association table (which i different than the one passed in if ( queryableCollection.isManyToMany() && queryableCollection.hasIndex() && SPECIAL_MANY2MANY_TREATMENT_FUNCTION_NAMES.contains( propertyName ) ) { return new SpecialManyToManyCollectionPropertyMapping(); } // If the property is a special collection property name, return a CollectionPropertyMapping. if ( CollectionProperties.isCollectionProperty( propertyName ) ) { if ( collectionPropertyMapping == null ) { collectionPropertyMapping = new CollectionPropertyMapping( queryableCollection ); } return collectionPropertyMapping; } if ( queryableCollection.getElementType().isAnyType() ) { // collection of <many-to-any/> mappings... // used to circumvent the component-collection check below... return queryableCollection; } if ( queryableCollection.getElementType().isComponentType() ) { // Collection of components. if ( propertyName.equals( EntityPersister.ENTITY_ID ) ) { return ( PropertyMapping ) queryableCollection.getOwnerEntityPersister(); } } return queryableCollection; } public boolean isCollectionOfValuesOrComponents() { return persister == null && queryableCollection != null && !queryableCollection.getElementType().isEntityType(); } public boolean isEntity() { return persister != null; } public ParameterSpecification getIndexCollectionSelectorParamSpec() { return indexCollectionSelectorParamSpec; } public void setIndexCollectionSelectorParamSpec(ParameterSpecification indexCollectionSelectorParamSpec) { this.indexCollectionSelectorParamSpec = indexCollectionSelectorParamSpec; } private class SpecialManyToManyCollectionPropertyMapping implements PropertyMapping { /** * {@inheritDoc} */ public Type getType() { return queryableCollection.getCollectionType(); } private void validate(String propertyName) { if ( ! ( CollectionPropertyNames.COLLECTION_INDEX.equals( propertyName ) || CollectionPropertyNames.COLLECTION_MAX_INDEX.equals( propertyName ) || CollectionPropertyNames.COLLECTION_MIN_INDEX.equals( propertyName ) ) ) { throw new IllegalArgumentException( "Expecting index-related function call" ); } } /** * {@inheritDoc} */ public Type toType(String propertyName) throws QueryException { validate( propertyName ); return queryableCollection.getIndexType(); } /** * {@inheritDoc} */ public String[] toColumns(String alias, String propertyName) throws QueryException { validate( propertyName ); final String joinTableAlias = joinSequence.getFirstJoin().getAlias(); if ( CollectionPropertyNames.COLLECTION_INDEX.equals( propertyName ) ) { return queryableCollection.toColumns( joinTableAlias, propertyName ); } final String[] cols = queryableCollection.getIndexColumnNames( joinTableAlias ); if ( CollectionPropertyNames.COLLECTION_MIN_INDEX.equals( propertyName ) ) { if ( cols.length != 1 ) { throw new QueryException( "composite collection index in minIndex()" ); } return new String[] { "min(" + cols[0] + ')' }; } else { if ( cols.length != 1 ) { throw new QueryException( "composite collection index in maxIndex()" ); } return new String[] { "max(" + cols[0] + ')' }; } } /** * {@inheritDoc} */ public String[] toColumns(String propertyName) throws QueryException, UnsupportedOperationException { validate( propertyName ); return queryableCollection.toColumns( propertyName ); } } }