/* * 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 <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.criterion; import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import org.hibernate.Criteria; import org.hibernate.EntityMode; import org.hibernate.engine.spi.TypedValue; import org.hibernate.internal.util.StringHelper; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; /** * Support for query by example. * * <pre> * List results = session.createCriteria(Parent.class) * .add( Example.create(parent).ignoreCase() ) * .createCriteria("child") * .add( Example.create( parent.getChild() ) ) * .list(); * </pre> * * "Examples" may be mixed and matched with "Expressions" in the same Criteria. * * @see org.hibernate.Criteria * @author Gavin King */ public class Example implements Criterion { private final Object exampleEntity; private PropertySelector selector; private boolean isLikeEnabled; private Character escapeCharacter; private boolean isIgnoreCaseEnabled; private MatchMode matchMode; private final Set<String> excludedProperties = new HashSet<String>(); /** * Create a new Example criterion instance, which includes all non-null properties by default * * @param exampleEntity The example bean to use. * * @return a new instance of Example */ public static Example create(Object exampleEntity) { if ( exampleEntity == null ) { throw new NullPointerException( "null example entity" ); } return new Example( exampleEntity, NotNullPropertySelector.INSTANCE ); } /** * Allow subclasses to instantiate as needed. * * @param exampleEntity The example bean * @param selector The property selector to use */ protected Example(Object exampleEntity, PropertySelector selector) { this.exampleEntity = exampleEntity; this.selector = selector; } /** * Set escape character for "like" clause if like matching was enabled * * @param escapeCharacter The escape character * * @return {@code this}, for method chaining * * @see #enableLike */ public Example setEscapeCharacter(Character escapeCharacter) { this.escapeCharacter = escapeCharacter; return this; } /** * Use the "like" operator for all string-valued properties. This form implicitly uses {@link MatchMode#EXACT} * * @return {@code this}, for method chaining */ public Example enableLike() { return enableLike( MatchMode.EXACT ); } /** * Use the "like" operator for all string-valued properties * * @param matchMode The match mode to use. * * @return {@code this}, for method chaining */ public Example enableLike(MatchMode matchMode) { this.isLikeEnabled = true; this.matchMode = matchMode; return this; } /** * Ignore case for all string-valued properties * * @return {@code this}, for method chaining */ public Example ignoreCase() { this.isIgnoreCaseEnabled = true; return this; } /** * Set the property selector to use. * * The property selector operates separate from excluding a property. * * @param selector The selector to use * * @return {@code this}, for method chaining * * @see #excludeProperty */ public Example setPropertySelector(PropertySelector selector) { this.selector = selector; return this; } /** * Exclude zero-valued properties. * * Equivalent to calling {@link #setPropertySelector} passing in {@link NotNullOrZeroPropertySelector#INSTANCE} * * @return {@code this}, for method chaining * * @see #setPropertySelector */ public Example excludeZeroes() { setPropertySelector( NotNullOrZeroPropertySelector.INSTANCE ); return this; } /** * Include all properties. * * Equivalent to calling {@link #setPropertySelector} passing in {@link AllPropertySelector#INSTANCE} * * @return {@code this}, for method chaining * * @see #setPropertySelector */ public Example excludeNone() { setPropertySelector( AllPropertySelector.INSTANCE ); return this; } /** * Exclude a particular property by name. * * @param name The name of the property to exclude * * @return {@code this}, for method chaining * * @see #setPropertySelector */ public Example excludeProperty(String name) { excludedProperties.add( name ); return this; } @Override public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) { final StringBuilder buf = new StringBuilder().append( '(' ); final EntityPersister meta = criteriaQuery.getFactory().getEntityPersister( criteriaQuery.getEntityName( criteria ) ); final String[] propertyNames = meta.getPropertyNames(); final Type[] propertyTypes = meta.getPropertyTypes(); final Object[] propertyValues = meta.getPropertyValues( exampleEntity ); for ( int i=0; i<propertyNames.length; i++ ) { final Object propertyValue = propertyValues[i]; final String propertyName = propertyNames[i]; final boolean isVersionProperty = i == meta.getVersionProperty(); if ( ! isVersionProperty && isPropertyIncluded( propertyValue, propertyName, propertyTypes[i] ) ) { if ( propertyTypes[i].isComponentType() ) { appendComponentCondition( propertyName, propertyValue, (CompositeType) propertyTypes[i], criteria, criteriaQuery, buf ); } else { appendPropertyCondition( propertyName, propertyValue, criteria, criteriaQuery, buf ); } } } if ( buf.length()==1 ) { buf.append( "1=1" ); } return buf.append( ')' ).toString(); } @SuppressWarnings("SimplifiableIfStatement") private boolean isPropertyIncluded(Object value, String name, Type type) { if ( excludedProperties.contains( name ) ) { // was explicitly excluded return false; } if ( type.isAssociationType() ) { // associations are implicitly excluded return false; } return selector.include( value, name, type ); } @Override public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuery) { final EntityPersister meta = criteriaQuery.getFactory().getEntityPersister( criteriaQuery.getEntityName( criteria ) ); final String[] propertyNames = meta.getPropertyNames(); final Type[] propertyTypes = meta.getPropertyTypes(); final Object[] values = meta.getPropertyValues( exampleEntity ); final List<TypedValue> list = new ArrayList<TypedValue>(); for ( int i=0; i<propertyNames.length; i++ ) { final Object value = values[i]; final Type type = propertyTypes[i]; final String name = propertyNames[i]; final boolean isVersionProperty = i == meta.getVersionProperty(); if ( ! isVersionProperty && isPropertyIncluded( value, name, type ) ) { if ( propertyTypes[i].isComponentType() ) { addComponentTypedValues( name, value, (CompositeType) type, list, criteria, criteriaQuery ); } else { addPropertyTypedValue( value, type, list ); } } } return list.toArray( new TypedValue[ list.size() ] ); } protected void addPropertyTypedValue(Object value, Type type, List<TypedValue> list) { if ( value != null ) { if ( value instanceof String ) { String string = (String) value; if ( isIgnoreCaseEnabled ) { string = string.toLowerCase(Locale.ROOT); } if ( isLikeEnabled ) { string = matchMode.toMatchString( string ); } value = string; } list.add( new TypedValue( type, value ) ); } } protected void addComponentTypedValues( String path, Object component, CompositeType type, List<TypedValue> list, Criteria criteria, CriteriaQuery criteriaQuery) { if ( component != null ) { final String[] propertyNames = type.getPropertyNames(); final Type[] subtypes = type.getSubtypes(); final Object[] values = type.getPropertyValues( component, getEntityMode( criteria, criteriaQuery ) ); for ( int i=0; i<propertyNames.length; i++ ) { final Object value = values[i]; final Type subtype = subtypes[i]; final String subpath = StringHelper.qualify( path, propertyNames[i] ); if ( isPropertyIncluded( value, subpath, subtype ) ) { if ( subtype.isComponentType() ) { addComponentTypedValues( subpath, value, (CompositeType) subtype, list, criteria, criteriaQuery ); } else { addPropertyTypedValue( value, subtype, list ); } } } } } private EntityMode getEntityMode(Criteria criteria, CriteriaQuery criteriaQuery) { final EntityPersister meta = criteriaQuery.getFactory().getEntityPersister( criteriaQuery.getEntityName( criteria ) ); final EntityMode result = meta.getEntityMode(); if ( ! meta.getEntityMetamodel().getTuplizer().isInstance( exampleEntity ) ) { throw new ClassCastException( exampleEntity.getClass().getName() ); } return result; } protected void appendPropertyCondition( String propertyName, Object propertyValue, Criteria criteria, CriteriaQuery cq, StringBuilder buf) { final Criterion condition; if ( propertyValue != null ) { final boolean isString = propertyValue instanceof String; if ( isLikeEnabled && isString ) { condition = new LikeExpression( propertyName, (String) propertyValue, matchMode, escapeCharacter, isIgnoreCaseEnabled ); } else { condition = new SimpleExpression( propertyName, propertyValue, "=", isIgnoreCaseEnabled && isString ); } } else { condition = new NullExpression(propertyName); } final String conditionFragment = condition.toSqlString( criteria, cq ); if ( conditionFragment.trim().length() > 0 ) { if ( buf.length() > 1 ) { buf.append( " and " ); } buf.append( conditionFragment ); } } protected void appendComponentCondition( String path, Object component, CompositeType type, Criteria criteria, CriteriaQuery criteriaQuery, StringBuilder buf) { if ( component != null ) { final String[] propertyNames = type.getPropertyNames(); final Object[] values = type.getPropertyValues( component, getEntityMode( criteria, criteriaQuery ) ); final Type[] subtypes = type.getSubtypes(); for ( int i=0; i<propertyNames.length; i++ ) { final String subPath = StringHelper.qualify( path, propertyNames[i] ); final Object value = values[i]; if ( isPropertyIncluded( value, subPath, subtypes[i] ) ) { final Type subtype = subtypes[i]; if ( subtype.isComponentType() ) { appendComponentCondition( subPath, value, (CompositeType) subtype, criteria, criteriaQuery, buf ); } else { appendPropertyCondition( subPath, value, criteria, criteriaQuery, buf ); } } } } } @Override public String toString() { return "example (" + exampleEntity + ')'; } // PropertySelector definitions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * A strategy for choosing property values for inclusion in the query criteria. Note that * property selection (for inclusion) operates separately from excluding a property. Excluded * properties are not even passed in to the PropertySelector for consideration. */ public static interface PropertySelector extends Serializable { /** * Determine whether the given property should be used in the criteria. * * @param propertyValue The property value (from the example bean) * @param propertyName The name of the property * @param type The type of the property * * @return {@code true} indicates the property should be included; {@code false} indiates it should not. */ public boolean include(Object propertyValue, String propertyName, Type type); } /** * Property selector that includes all properties */ public static final class AllPropertySelector implements PropertySelector { /** * Singleton access */ public static final AllPropertySelector INSTANCE = new AllPropertySelector(); @Override public boolean include(Object object, String propertyName, Type type) { return true; } private Object readResolve() { return INSTANCE; } } /** * Property selector that includes only properties that are not {@code null} */ public static final class NotNullPropertySelector implements PropertySelector { /** * Singleton access */ public static final NotNullPropertySelector INSTANCE = new NotNullPropertySelector(); @Override public boolean include(Object object, String propertyName, Type type) { return object!=null; } private Object readResolve() { return INSTANCE; } } /** * Property selector that includes only properties that are not {@code null} and non-zero (if numeric) */ public static final class NotNullOrZeroPropertySelector implements PropertySelector { /** * Singleton access */ public static final NotNullOrZeroPropertySelector INSTANCE = new NotNullOrZeroPropertySelector(); @Override public boolean include(Object object, String propertyName, Type type) { return object != null && ( !(object instanceof Number) || ( (Number) object ).longValue()!=0 ); } private Object readResolve() { return INSTANCE; } } }