package com.webobjects.jdbcadaptor;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.webobjects.eoaccess.EOAttribute;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eocontrol.EOKeyComparisonQualifier;
import com.webobjects.eocontrol.EOKeyValueQualifier;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOQualifierVariable;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSSelector;
import com.webobjects.foundation._NSStringUtilities;
/**
* <p>
* Overrides the default EOF MySQLExpression to adjust the sql generation of
* string comparisons.
* </p>
* <p>
* MySQL's default behaviour, as per usual, is to ignore the standards. Thus an
* sql <code>like</code> performs a case insensitive comparison rather than case
* sensitive which is vexing.
* </p>
* <p>
* To enforce standard behaviour you can tell mysql by using
* <code>like binary</code> for a case sensitive comparison. Thus a case
* insensitive comparison simply requires using a <code>like</code> in order to
* obtain the results desired.
* </p>
* <p>
* <b>Note</b>: This assumes that you're not using binary columns.
* </p>
* <p>
* The alternative is to define all of your columns as binary columns and
* specifically specify the collation to use for case insensitive comparisons.
* But that's a more complex approach in the author's view.
* </p>
* <p>
* Another approach is to only ever use qualifiers and sort orderings that are
* case sensitive (semantically) and choose in your model what external type to
* map to in order to control the behaviour. In my view this is bad practice
* because you're separating the logic of queries and returning results that are
* not intended according to the code.
* </p>
* <p>
* <p>
* To summarise, if this class is enabled:
* </p>
* <ul>
* <li>LIKE UPPER(foo) becomes LIKE foo
* <li>LIKE foo becomes LIKE BINARY foo
* </ul>
*
* @property com.webobjects.jdbcadaptor.MySQLExpression.enable set to <code>true</code> to enable this class and change the behavior of the
* <code>like</code> operator to be case-insensitive.
*
* @author ldeck
*/
public class MySQLExpression
extends
com.webobjects.jdbcadaptor.MySQLPlugIn.MySQLExpression
{
private static final List< NSSelector > SORT_ORDERING_ASC_SELECTORS =
Arrays.asList( EOSortOrdering.CompareAscending, EOSortOrdering.CompareCaseInsensitiveAscending );
private static final List< NSSelector > SORT_ORDERING_BIN_SELECTORS = Arrays.asList( EOSortOrdering.CompareAscending, EOSortOrdering.CompareDescending );
private static final List< NSSelector > SORT_ORDERING_DESC_SELECTORS =
Arrays.asList( EOSortOrdering.CompareCaseInsensitiveDescending, EOSortOrdering.CompareDescending );
private final Pattern likeOperatorRegex;
private final Pattern upperFunctionNameRegex;
/**
* @param entity
*/
public MySQLExpression( EOEntity entity )
{
super( entity );
upperFunctionNameRegex = Pattern.compile( "\\Q" + _upperFunctionName + "\\E\\(([^\\)]+)\\)" );
likeOperatorRegex = Pattern.compile( "([Ll][Ii][Kk][Ee])" );
}
/**
* @see com.webobjects.eoaccess.EOSQLExpression#addOrderByAttributeOrdering(com.webobjects.eocontrol.EOSortOrdering)
*/
@Override
public void addOrderByAttributeOrdering( EOSortOrdering sortOrdering )
{
NSSelector< ? > selector = sortOrdering.selector();
String attPath = sortOrdering.key();
String sqlString1 = sqlStringForAttributeNamed( attPath );
if ( sqlString1 == null )
{
throw new IllegalStateException( new StringBuilder()
.append( "addOrderByAttributeOrdering: attempt to generate SQL for " )
.append( sortOrdering.getClass().getName() )
.append( " " )
.append( sortOrdering )
.append( " failed because attribute identified by key '" )
.append( sortOrdering.key() )
.append( "' was not reachable from from entity '" )
.append( _entity.name() )
.append( "'" )
.toString() );
}
String format;
boolean isAscending = false;
if ( ( isAscending = SORT_ORDERING_ASC_SELECTORS.contains( selector ) ) || SORT_ORDERING_DESC_SELECTORS.contains( selector ) )
{
String orderType = isAscending ? "ASC" : "DESC";
String binaryOperator =
SORT_ORDERING_BIN_SELECTORS.contains( selector )
&& entity()._attributeForPath( attPath ).adaptorValueType() == EOAttribute.AdaptorCharactersType ? "BINARY " : "";
format = _NSStringUtilities.concat( binaryOperator, " ", sqlString1, " ", orderType );
}
else
{
format = _NSStringUtilities.concat( "(", sqlString1, ")" );
}
appendItemToListString( format, _orderByString() );
}
/**
* @see com.webobjects.jdbcadaptor.JDBCExpression#appendItemToOrderByString(java.lang.String)
*/
@Override
protected void appendItemToOrderByString( String sqlString )
{
super.appendItemToOrderByString( replaceStringForCaseInsensitiveLike( sqlString ) );
}
protected Pattern likeOperatorRegex()
{
return likeOperatorRegex;
}
protected String replaceStringForCaseInsensitiveLike( String string )
{
StringBuffer result = new StringBuffer();
Matcher matcher = upperFunctionNameRegex().matcher( string );
while ( matcher.find() )
{
matcher.appendReplacement( result, Matcher.quoteReplacement( matcher.group( 1 ) ) );
}
matcher.appendTail( result );
return result.toString();
}
protected String replaceStringForCaseSensitiveLike( String string )
{
StringBuffer result = new StringBuffer();
Matcher matcher = likeOperatorRegex().matcher( string );
while ( matcher.find() )
{
matcher.appendReplacement( result, Matcher.quoteReplacement( matcher.group( 1 ) + " BINARY" ) );
}
matcher.appendTail( result );
return result.toString();
}
/**
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForCaseInsensitiveLike(java.lang.String,
* java.lang.String)
*/
@Override
public String sqlStringForCaseInsensitiveLike( String valueString, String keyString )
{
return replaceStringForCaseInsensitiveLike( super.sqlStringForCaseInsensitiveLike( valueString, keyString ) );
}
/**
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForKeyComparisonQualifier(com.webobjects.eocontrol.EOKeyComparisonQualifier)
*/
@Override
public String sqlStringForKeyComparisonQualifier( EOKeyComparisonQualifier qualifier )
{
String leftKey = qualifier.leftKey();
String rightKey = qualifier.rightKey();
if ( leftKey != null && leftKey.equals( rightKey ) )
{
return "(1=1)";
}
EOAttribute att = _entity._attributeForPath( leftKey );
String leftKeyString = sqlStringForAttributeNamed( leftKey );
if ( leftKeyString == null )
{
throw new IllegalStateException( new StringBuilder()
.append( "sqlStringForKeyComparisonQualifier: attempt to generate SQL for " )
.append( qualifier.getClass().getName() )
.append( " " )
.append( qualifier )
.append( " failed because attribute identified by key '" )
.append( leftKey )
.append( "' was not reachable from from entity '" )
.append( _entity.name() )
.append( "'" )
.toString() );
}
leftKeyString = formatSQLString( leftKeyString, att.readFormat() );
att = _entity._attributeForPath( rightKey );
String rightKeyString = sqlStringForAttributeNamed( rightKey );
if ( rightKeyString == null )
{
throw new IllegalStateException( new StringBuilder()
.append( "sqlStringForKeyComparisonQualifier: attempt to generate SQL for " )
.append( qualifier.getClass().getName() )
.append( " " )
.append( qualifier )
.append( " failed because attribute identified by key '" )
.append( rightKey )
.append( "' was not reachable from from entity '" )
.append( _entity.name() )
.append( "'" )
.toString() );
}
else
{
rightKeyString = formatSQLString( rightKeyString, att.readFormat() );
String operatorString = sqlStringForSelector( qualifier.selector(), null );
EOAttribute leftAttribute = _entity._attributeForPath( leftKey );
EOAttribute rightAttribute = _entity._attributeForPath( rightKey );
NSSelector qualifierSelector = qualifier.selector();
boolean isLike =
qualifierSelector.equals( EOQualifier.QualifierOperatorLike ) || qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike );
boolean isStringComparison =
isLike
|| EOAttribute.AdaptorCharactersType == leftAttribute.adaptorValueType()
|| EOAttribute.AdaptorCharactersType == rightAttribute.adaptorValueType();
String binaryOperator = isStringComparison ? "BINARY " : "";
return _NSStringUtilities.concat( binaryOperator, leftKeyString, " ", operatorString, " ", rightKeyString );
}
}
/**
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForKeyValueQualifier(com.webobjects.eocontrol.EOKeyValueQualifier)
*/
@Override
public String sqlStringForKeyValueQualifier( EOKeyValueQualifier qualifier )
{
String key = qualifier.key();
String keyString = sqlStringForAttributeNamed( key );
if ( keyString == null )
{
throw new IllegalStateException( new StringBuilder()
.append( "sqlStringForKeyValueQualifier: attempt to generate SQL for " )
.append( qualifier.getClass().getName() )
.append( " " )
.append( qualifier )
.append( " failed because attribute identified by key '" )
.append( key )
.append( "' was not reachable from from entity '" )
.append( _entity.name() )
.append( "'" )
.toString() );
}
Object qualifierValue = qualifier.value();
if ( qualifierValue instanceof EOQualifierVariable )
{
throw new IllegalStateException( new StringBuilder()
.append( "sqlStringForKeyValueQualifier: attempt to generate SQL for " )
.append( qualifier.getClass().getName() )
.append( " " )
.append( qualifier )
.append( " failed because the qualifier variable '$" )
.append( ( ( EOQualifierVariable )qualifierValue ).key() )
.append( "' is unbound." )
.toString() );
}
EOAttribute attribute = _entity._attributeForPath( key );
keyString = formatSQLString( keyString, attribute.readFormat() );
NSSelector qualifierSelector = qualifier.selector();
boolean isLike =
qualifierSelector.equals( EOQualifier.QualifierOperatorLike ) || qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike );
boolean isStringComparison = isLike || EOAttribute.AdaptorCharactersType == attribute.adaptorValueType();
Object value;
if ( isLike )
{
value = sqlPatternFromShellPattern( ( String )qualifierValue );
}
else
{
value = qualifierValue;
}
String string;
if ( qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike ) )
{
String valueString = sqlStringForValue( value, key );
String operatorString = sqlStringForSelector( qualifierSelector, value );
string = sqlStringForCaseInsensitiveLike( valueString, keyString );
}
else if ( EOQualifier.QualifierOperatorLike.equals( qualifierSelector ) || isStringComparison )
{
String valueString = sqlStringForValue( value, key );
String operatorString = sqlStringForSelector( qualifierSelector, value );
string = _NSStringUtilities.concat( "BINARY ", keyString, " ", operatorString, " ", valueString );
}
else
{
String valueString = sqlStringForValue( value, key );
String operatorString = sqlStringForSelector( qualifierSelector, value );
string = _NSStringUtilities.concat( keyString, " ", operatorString, " ", valueString );
}
if ( isLike )
{
char escapeChar = sqlEscapeChar();
if ( escapeChar != 0 )
{
string = _NSStringUtilities.concat( string, new StringBuilder().append( " ESCAPE '" ).append( escapeChar ).append( "'" ).toString() );
}
}
return string;
}
protected Pattern upperFunctionNameRegex()
{
return upperFunctionNameRegex;
}
}