diff --git a/Frameworks/PlugIns/MySQLPlugIn/Sources/com/webobjects/jdbcadaptor/MySQLExpression.java b/Frameworks/PlugIns/MySQLPlugIn/Sources/com/webobjects/jdbcadaptor/MySQLExpression.java new file mode 100644 index 00000000000..924b29838fc --- /dev/null +++ b/Frameworks/PlugIns/MySQLPlugIn/Sources/com/webobjects/jdbcadaptor/MySQLExpression.java @@ -0,0 +1,341 @@ +/** + * + */ +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; + +/** + *

+ * Overrides the default EOF MySQLExpression to adjust the sql generation of + * string comparisons. + *

+ *

+ * MySQL's default behaviour, as per usual, is to ignore the standards. Thus an + * sql like performs a case insensitive comparison rather than case + * sensitive which is vexing. + *

+ *

+ * To enforce standard behaviour you can tell mysql by using + * like binary for a case sensitive comparison. Thus a case + * insensitive comparison simply requires using a like in order to + * obtain the results desired. + *

+ *

+ * Note: This assumes that you're not using binary columns. + *

+ *

+ * 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. + *

+ *

+ * Another approach is to only every 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. + *

+ *

+ * To have this class enabled as the runtime MySQLExpression define the + * following property. + *

+ *

+ * com.webobjects.jdbcadaptor.MySQLExpression.enable=true + *

+ *

+ * To summarise: + *

+ * + * + * @author ldeck + */ +public class MySQLExpression + extends + com.webobjects.jdbcadaptor.MySQLPlugIn.MySQLExpression +{ + + private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger( MySQLExpression.class ); + 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 ); + this.upperFunctionNameRegex = Pattern.compile( "\\Q" + this._upperFunctionName + "\\E\\(([^\\)]+)\\)" ); + this.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( this._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 this.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 = this._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( this._entity.name() ) + .append( "'" ) + .toString() ); + } + leftKeyString = formatSQLString( leftKeyString, att.readFormat() ); + att = this._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( this._entity.name() ) + .append( "'" ) + .toString() ); + } + else + { + rightKeyString = formatSQLString( rightKeyString, att.readFormat() ); + String operatorString = sqlStringForSelector( qualifier.selector(), null ); + + EOAttribute leftAttribute = this._entity._attributeForPath( leftKey ); + EOAttribute rightAttribute = this._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( this._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 = this._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 this.upperFunctionNameRegex; + } + +} diff --git a/Frameworks/PlugIns/MySQLPlugIn/Sources/com/webobjects/jdbcadaptor/_MySQLPlugIn.java b/Frameworks/PlugIns/MySQLPlugIn/Sources/com/webobjects/jdbcadaptor/_MySQLPlugIn.java index 651de8ae20b..0e333e1c3a9 100644 --- a/Frameworks/PlugIns/MySQLPlugIn/Sources/com/webobjects/jdbcadaptor/_MySQLPlugIn.java +++ b/Frameworks/PlugIns/MySQLPlugIn/Sources/com/webobjects/jdbcadaptor/_MySQLPlugIn.java @@ -25,6 +25,7 @@ import com.webobjects.foundation.NSLog; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; +import com.webobjects.foundation.NSProperties; import com.webobjects.foundation.NSPropertyListSerialization; import com.webobjects.foundation._NSStringUtilities; @@ -312,6 +313,9 @@ public String databaseProductName() { @Override public Class defaultExpressionClass() { + if (NSProperties.booleanForKeyWithDefault("com.webobjects.jdbcadaptor.MySQLExpression.enable", false)) { + return com.webobjects.jdbcadaptor.MySQLExpression.class; + } return com.webobjects.jdbcadaptor._MySQLPlugIn.MySQLExpression.class; }