All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hibernate.query.internal.QueryParameterBindingsImpl Maven / Gradle / Ivy

There is a newer version: 7.0.0.Alpha1
Show newest version
/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or .
 */
package org.hibernate.query.internal;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

import org.hibernate.HibernateException;
import org.hibernate.Incubating;
import org.hibernate.QueryException;
import org.hibernate.QueryParameterException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.query.spi.NamedParameterDescriptor;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.hql.internal.classic.ParserHelper;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.query.ParameterMetadata;
import org.hibernate.query.QueryParameter;
import org.hibernate.query.spi.QueryParameterBinding;
import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.query.spi.QueryParameterListBinding;
import org.hibernate.type.SerializableType;
import org.hibernate.type.Type;

/**
 * Manages the group of QueryParameterBinding for a particular query.
 *
 * @author Steve Ebersole
 * @author Chris Cranford
 */
@Incubating
public class QueryParameterBindingsImpl implements QueryParameterBindings {
	private static final CoreMessageLogger log = CoreLogging.messageLogger( QueryParameterBindingsImpl.class );

	private final SessionFactoryImplementor sessionFactory;
	private final ParameterMetadata parameterMetadata;
	private final boolean queryParametersValidationEnabled;

	private Map parameterBindingMap;
	private Map parameterListBindingMap;
	private Map positionalParameterBindings;

	public static QueryParameterBindingsImpl from(
			ParameterMetadata parameterMetadata,
			SessionFactoryImplementor sessionFactory,
			boolean queryParametersValidationEnabled) {
		if ( parameterMetadata == null ) {
			return new QueryParameterBindingsImpl(
					sessionFactory,
					parameterMetadata,
					queryParametersValidationEnabled
			);
		}
		else {
			return new QueryParameterBindingsImpl(
					sessionFactory,
					parameterMetadata.collectAllParameters(),
					parameterMetadata, queryParametersValidationEnabled
			);
		}
	}

	private QueryParameterBindingsImpl(
			SessionFactoryImplementor sessionFactory,
			ParameterMetadata parameterMetadata,
			boolean queryParametersValidationEnabled) {
		this( sessionFactory, Collections.emptySet(), parameterMetadata, queryParametersValidationEnabled );
	}

	private QueryParameterBindingsImpl(
			SessionFactoryImplementor sessionFactory,
			Set> queryParameters,
			ParameterMetadata parameterMetadata,
			boolean queryParametersValidationEnabled) {
		this.sessionFactory = sessionFactory;
		this.parameterMetadata = parameterMetadata;
		this.queryParametersValidationEnabled = queryParametersValidationEnabled;
		this.positionalParameterBindings = new TreeMap<>(  );

		if ( queryParameters == null || queryParameters.isEmpty() ) {
			parameterBindingMap = Collections.emptyMap();
		}
		else {
			parameterBindingMap = new HashMap<>();

			for ( QueryParameter queryParameter : queryParameters ) {
				if ( queryParameter.getPosition() == null ) {
					// only cache the non-positional parameters in this map
					// positional parameters will be bound dynamically with getBinding(int)
					parameterBindingMap.put( queryParameter, makeBinding( queryParameter ) );
				}
			}
		}

		parameterListBindingMap = new HashMap<>();
	}

	protected QueryParameterBinding makeBinding(QueryParameter queryParameter) {
		return makeBinding( queryParameter.getType() );
	}

	protected QueryParameterBinding makeBinding(Type bindType) {
		return new QueryParameterBindingImpl( bindType, sessionFactory, shouldValidateBindingValue() );
	}

	public boolean isBound(QueryParameter parameter) {
		final QueryParameterBinding binding = locateBinding( parameter );
		if ( binding != null ) {
			return binding.getBindValue() != null;
		}

		final QueryParameterListBinding listBinding = locateQueryParameterListBinding( parameter );
		if ( listBinding != null ) {
			return listBinding.getBindValues() != null;
		}

		return false;
	}

	@SuppressWarnings("unchecked")
	public  QueryParameterBinding getBinding(QueryParameter parameter) {
		final QueryParameterBinding binding = locateBinding( parameter );

		if ( binding == null ) {
			throw new IllegalArgumentException(
					"Could not resolve QueryParameter reference [" + parameter + "] to QueryParameterBinding"
			);
		}

		return binding;
	}

	@SuppressWarnings("unchecked")
	public  QueryParameterBinding locateBinding(QueryParameter parameter) {
		// see if this exact instance is known as a key
		if ( parameterBindingMap.containsKey( parameter ) ) {
			return parameterBindingMap.get( parameter );
		}

		// if the incoming parameter has a name, try to find it by name
		if ( StringHelper.isNotEmpty( parameter.getName() ) ) {
			final QueryParameterBinding binding = locateBinding( parameter.getName() );
			if ( binding != null ) {
				return binding;
			}
		}

		// if the incoming parameter has a position, try to find it by position
		if ( parameter.getPosition() != null ) {
			final QueryParameterBinding binding = locateBinding( parameter.getPosition() );
			if ( binding != null ) {
				return binding;
			}
		}

		return null;
	}

	protected QueryParameterBinding locateBinding(String name) {
		for ( Map.Entry entry : parameterBindingMap.entrySet() ) {
			if ( name.equals( entry.getKey().getName() ) ) {
				return entry.getValue();
			}
		}

		return null;
	}

	protected QueryParameterBinding locateAndRemoveBinding(String name) {
		final Iterator> entryIterator = parameterBindingMap.entrySet().iterator();
		while ( entryIterator.hasNext() ) {
			final Map.Entry entry = entryIterator.next();
			if ( name.equals( entry.getKey().getName() ) ) {
				entryIterator.remove();
				return entry.getValue();
			}
		}

		return null;
	}

	protected QueryParameterBinding locateBinding(int position) {
		if ( position < positionalParameterBindings.size() ) {
			return positionalParameterBindings.get( position );
		}

		return null;
	}

	public QueryParameterBinding getBinding(String name) {
		final QueryParameterBinding binding = locateBinding( name );
		if ( binding == null ) {
			throw new IllegalArgumentException( "Unknown parameter name : " + name );
		}

		return binding;
	}

	public QueryParameterBinding getBinding(int position) {
		int positionAdjustment = 0;
		if ( !parameterMetadata.isOrdinalParametersZeroBased() ) {
			positionAdjustment = -1;
		}
		QueryParameterBinding binding = null;
		if ( parameterMetadata != null ) {
			if ( !parameterMetadata.hasPositionalParameters() ) {
				// no positional parameters, assume jpa named.
				binding = locateBinding( Integer.toString( position ) );
			}
			else {
				try {
					binding = positionalParameterBindings.get( position + positionAdjustment );
					if ( binding == null ) {
						binding = makeBinding( parameterMetadata.getQueryParameter( position ) );
						positionalParameterBindings.put( position + positionAdjustment, binding );
					}
				}
				catch (QueryParameterException e) {
					// treat this as null binding
				}
			}
		}

		if ( binding == null ) {
			throw new IllegalArgumentException( "Unknown parameter position: " + position );
		}

		return binding;
	}

	public void verifyParametersBound(boolean reserveFirstParameter) {
		// verify named parameters bound
		for ( Map.Entry bindEntry : parameterBindingMap.entrySet() ) {
			if ( !bindEntry.getValue().isBound() ) {
				if ( bindEntry.getKey().getName() != null ) {
					throw new QueryException( "Named parameter [" + bindEntry.getKey().getName() + "] not set" );
				}
				else {
					throw new QueryException( "Parameter memento [" + bindEntry.getKey() + "] not set" );
				}
			}
		}
		// verify position parameters bound
		int startIndex = 0;
		if ( !parameterMetadata.isOrdinalParametersZeroBased() ) {
			startIndex = 1;
		}
		for ( int i = startIndex; i < positionalParameterBindings.size(); i++ ) {
			QueryParameterBinding binding = null;
			if ( parameterMetadata.isOrdinalParametersZeroBased() ) {
				binding = positionalParameterBindings.get( i );
			}
			else {
				binding = positionalParameterBindings.get( i - 1 );
			}
			if ( binding == null || !binding.isBound() ) {
				throw new QueryException( "Positional parameter [" + i + "] not set" );
			}
		}
		// verify position parameter count is correct
		final int positionalValueSpan = calculatePositionalValueSpan( reserveFirstParameter );
		final int positionCounts = parameterMetadata.getPositionalParameterCount();
		if ( positionCounts != positionalValueSpan ) {
			if ( reserveFirstParameter && positionCounts - 1 != positionalValueSpan ) {
				throw new QueryException(
						"Expected positional parameter count: " +
								( positionCounts - 1 ) +
								", actually detected " + positionalValueSpan
				);
			}
			else if ( !reserveFirstParameter ) {
				throw new QueryException(
						"Expected positional parameter count: " +
								( positionCounts ) +
								", actually detected " + positionalValueSpan
				);
			}
		}
	}

	private int calculatePositionalValueSpan(boolean reserveFirstParameter) {
		int positionalValueSpan = 0;
		for ( QueryParameterBinding binding : positionalParameterBindings.values() ) {
			if ( binding.isBound() ) {
				Type bindType = binding.getBindType();
				if ( bindType == null ) {
					bindType = SerializableType.INSTANCE;
				}
				positionalValueSpan += bindType.getColumnSpan( sessionFactory );
			}
		}
		return positionalValueSpan;
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Collection collectBindTypes() {
		return parameterBindingMap.values()
				.stream()
				.map( QueryParameterBinding::getBindType )
				.collect( Collectors.toList() );
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Collection collectBindValues() {
		return parameterBindingMap.values()
				.stream()
				.map( QueryParameterBinding::getBindValue )
				.collect( Collectors.toList() );
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Type[] collectPositionalBindTypes() {
		Type[] types = new Type[ positionalParameterBindings.size() ];

		// NOTE : bindings should be ordered by position by nature of a TreeMap...
		// NOTE : we also assume the contiguity of the positions

		for ( Map.Entry entry : positionalParameterBindings.entrySet() ) {
			final int position = entry.getKey();

			Type type = entry.getValue().getBindType();
			if ( type == null ) {
				log.debugf( "Binding for positional-parameter [%s] did not define type, using SerializableType", position );
				type = SerializableType.INSTANCE;
			}

			types[ position ] = type;
		}

		return types;
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Object[] collectPositionalBindValues() {
		Object[] values = new Object[ positionalParameterBindings.size() ];

		// NOTE : bindings should be ordered by position by nature of a TreeMap...
		// NOTE : we also assume the contiguity of the positions

		for ( Map.Entry entry : positionalParameterBindings.entrySet() ) {
			final int position = entry.getKey();
			values[ position ] = entry.getValue().getBindValue();
		}

		return values;
	}

	/**
	 * @deprecated (since 5.2) expect a different approach to org.hibernate.engine.spi.QueryParameters in 6.0
	 */
	@Deprecated
	public Map collectNamedParameterBindings() {
		Map collectedBindings = new HashMap<>();
		for ( Map.Entry entry : parameterBindingMap.entrySet() ) {
			if ( entry.getKey().getName() == null ) {
				continue;
			}

			Type bindType = entry.getValue().getBindType();
			if ( bindType == null ) {
				log.debugf( "Binding for named-parameter [%s] did not define type", entry.getKey().getName() );
				bindType = SerializableType.INSTANCE;
			}

			collectedBindings.put(
					entry.getKey().getName(),
					new TypedValue( bindType, entry.getValue().getBindValue() )
			);
		}

		return collectedBindings;
	}


	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// Parameter list binding - expect changes in 6.0

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	public  QueryParameterListBinding getQueryParameterListBinding(QueryParameter queryParameter) {
		QueryParameterListBinding result = parameterListBindingMap.get( queryParameter );
		if ( result == null ) {
			result = transformQueryParameterBindingToQueryParameterListBinding( queryParameter );
		}
		return result;
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	private QueryParameterListBinding locateQueryParameterListBinding(QueryParameter queryParameter) {
		QueryParameterListBinding result = parameterListBindingMap.get( queryParameter );

		if ( result == null && queryParameter.getName() != null ) {
			for ( Map.Entry entry : parameterListBindingMap.entrySet() ) {
				if ( queryParameter.getName().equals( entry.getKey().getName() ) ) {
					result = entry.getValue();
					break;
				}
			}
		}

		return result;
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	private  QueryParameterListBinding transformQueryParameterBindingToQueryParameterListBinding(QueryParameter queryParameter) {
		log.debugf( "Converting QueryParameterBinding to QueryParameterListBinding for given QueryParameter : %s", queryParameter );
		final QueryParameterBinding binding = getAndRemoveBinding( queryParameter );
		if ( binding == null ) {
			throw new IllegalArgumentException(
					"Could not locate QueryParameterBinding for given QueryParameter : " + queryParameter +
							"; parameter list must be defined using named parameter"
			);
		}

		final QueryParameterListBinding convertedBinding = new QueryParameterListBindingImpl<>(
				binding.getBindType(),
				shouldValidateBindingValue()
		);
		parameterListBindingMap.put( queryParameter, convertedBinding );

		return convertedBinding;
	}

	private boolean shouldValidateBindingValue() {
		return sessionFactory.getSessionFactoryOptions().isJpaBootstrap() && queryParametersValidationEnabled;
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	private  QueryParameterBinding getAndRemoveBinding(QueryParameter parameter) {
		// see if this exact instance is known as a key
		if ( parameterBindingMap.containsKey( parameter ) ) {
			return parameterBindingMap.remove( parameter );
		}

		// if the incoming parameter has a name, try to find it by name
		if ( StringHelper.isNotEmpty( parameter.getName() ) ) {
			final QueryParameterBinding binding = locateAndRemoveBinding( parameter.getName() );
			if ( binding != null ) {
				return binding;
			}
		}

		// NOTE : getAndRemoveBinding is only intended for usage from #transformQueryParameterBindingToQueryParameterListBinding
		//		which only supports named parameters, so there is no need to look into legacy positional parameters

		throw new IllegalArgumentException(
				"Could not resolve QueryParameter reference [" + parameter + "] to QueryParameterBinding"
		);
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	public  QueryParameterListBinding getQueryParameterListBinding(String name) {
		// find the QueryParameter instance for the given name
		final QueryParameter queryParameter = resolveQueryParameter( name );
		return getQueryParameterListBinding( queryParameter );
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	private  QueryParameter resolveQueryParameter(String name) {
		for ( QueryParameter queryParameter : parameterListBindingMap.keySet() ) {
			if ( name.equals( queryParameter.getName() ) ) {
				return queryParameter;
			}
		}

		for ( QueryParameter queryParameter : parameterBindingMap.keySet() ) {
			if ( name.equals( queryParameter.getName() ) ) {
				return queryParameter;
			}
		}

		throw new IllegalArgumentException(
				"Unable to resolve given parameter name [" + name + "] to QueryParameter reference"
		);
	}

	/**
	 * @deprecated (since 5.2) expected changes to "collection-valued parameter binding" in 6.0
	 */
	@Deprecated
	@SuppressWarnings("unchecked")
	public String expandListValuedParameters(String queryString, SharedSessionContractImplementor session) {
		if ( queryString == null ) {
			return null;
		}

		// more-or-less... for each entry in parameterListBindingMap we will create an
		//		entry in parameterBindingMap for each of the values in the bound value list.  afterwards
		//		we will clear the parameterListBindingMap.
		//
		// NOTE that this is essentially the legacy logical prior to modeling QueryParameterBinding/QueryParameterListBinding.
		// 		Fully expect the details of how this is handled in 6.0

		// HHH-1123
		// Some DBs limit number of IN expressions.  For now, warn...
		final Dialect dialect = session.getFactory().getServiceRegistry().getService( JdbcServices.class ).getJdbcEnvironment().getDialect();
		final int inExprLimit = dialect.getInExpressionCountLimit();

		for ( Map.Entry entry : parameterListBindingMap.entrySet() ) {
			final NamedParameterDescriptor sourceParam = (NamedParameterDescriptor) entry.getKey();
			final Collection bindValues = entry.getValue().getBindValues();

			if ( inExprLimit > 0 && bindValues.size() > inExprLimit ) {
				log.tooManyInExpressions( dialect.getClass().getName(), inExprLimit, sourceParam.getName(), bindValues.size() );
			}

			final boolean isJpaPositionalParam = sourceParam.isJpaPositionalParameter();
			final String paramPrefix = isJpaPositionalParam ? "?" : ParserHelper.HQL_VARIABLE_PREFIX;
			final String placeholder = paramPrefix + sourceParam.getName();
			final int loc = queryString.indexOf( placeholder );

			if ( loc < 0 ) {
				continue;
			}

			final String beforePlaceholder = queryString.substring( 0, loc );
			final String afterPlaceholder = queryString.substring( loc + placeholder.length() );

			// check if placeholder is already immediately enclosed in parentheses
			// (ignoring whitespace)
			boolean isEnclosedInParens =
					StringHelper.getLastNonWhitespaceCharacter( beforePlaceholder ) == '(' &&
							StringHelper.getFirstNonWhitespaceCharacter( afterPlaceholder ) == ')';

			if ( bindValues.size() == 1 && isEnclosedInParens ) {
				// short-circuit for performance when only 1 value and the
				// placeholder is already enclosed in parentheses...
				final QueryParameterBinding syntheticBinding = makeBinding( entry.getValue().getBindType() );
				syntheticBinding.setBindValue( bindValues.iterator().next() );
				parameterBindingMap.put( sourceParam, syntheticBinding );
				continue;
			}

			StringBuilder expansionList = new StringBuilder();

			int i = 0;
			for ( Object bindValue : entry.getValue().getBindValues() ) {
				// for each value in the bound list-of-values we:
				//		1) create a synthetic named parameter
				//		2) expand the queryString to include each synthetic named param in place of the original
				//		3) create a new synthetic binding for just that single value under the synthetic name
				final String syntheticName = ( isJpaPositionalParam ? 'x' : "" ) + sourceParam.getName() + '_' + i;
				if ( i > 0 ) {
					expansionList.append( ", " );
				}
				expansionList.append( ParserHelper.HQL_VARIABLE_PREFIX ).append( syntheticName );
				final QueryParameter syntheticParam = new QueryParameterNamedImpl<>(
						syntheticName,
						sourceParam.getSourceLocations(),
						sourceParam.isJpaPositionalParameter(),
						sourceParam.getType()
				);
				final QueryParameterBinding syntheticBinding = makeBinding( entry.getValue().getBindType() );
				syntheticBinding.setBindValue( bindValue );
				parameterBindingMap.put( syntheticParam, syntheticBinding );
				i++;
			}

			queryString = StringHelper.replace(
					beforePlaceholder,
					afterPlaceholder,
					placeholder,
					expansionList.toString(),
					true,
					true
			);
		}

		return queryString;
	}
}