Skip to content

Commit

Permalink
Merge pull request #829 from rparada/sqlgen
Browse files Browse the repository at this point in the history
SQL generation fix for EOKeyValueQualifiers when key is key path with derived attribute
  • Loading branch information
darkv authored Mar 21, 2017
2 parents edc76b5 + 6aad1f0 commit bee51ac
Showing 1 changed file with 289 additions and 0 deletions.
289 changes: 289 additions & 0 deletions Frameworks/Core/ERExtensions/Sources/er/extensions/ERXExtensions.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

Expand All @@ -39,16 +41,20 @@
import com.webobjects.eocontrol.EOFetchSpecification;
import com.webobjects.eocontrol.EOKeyValueQualifier;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOQualifierVariable;
import com.webobjects.eocontrol.EOSharedEditingContext;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSBundle;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSForwardException;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSLog;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSNotification;
import com.webobjects.foundation.NSNotificationCenter;
import com.webobjects.foundation.NSSelector;
import com.webobjects.foundation._NSStringUtilities;
import com.webobjects.jdbcadaptor.JDBCAdaptorException;

import er.extensions.appserver.ERXApplication;
Expand Down Expand Up @@ -334,9 +340,18 @@ public static synchronized void registerSQLSupportForSelector(NSSelector selecto
* that was registered and uses their support instead.
* You'll use this mainly to bind queryOperators in display groups.
* @author ak
*
* Added SQL generation fix for EOKeyValueQualifiers in an edge case
* where the key is a key path (with two or more keys) and the last key
* is a derived attribute. For example, "customer.fullName" where the
* last key fullName is defined as: 'firstName || ' ' || lastName.
*
* @author Ricardo Parada
*/
public static class KeyValueQualifierSQLGenerationSupport extends EOQualifierSQLGeneration.Support {

public static final String HANDLES_KEY_PATH_WITH_DERIVED_ATTRIBUTE_PROPERTY_NAME = "er.extensions.KeyValueQualifierSQLGenerationSupport.handlesKeyPathWithDerivedAttribute";

private EOQualifierSQLGeneration.Support _old;

public KeyValueQualifierSQLGenerationSupport(EOQualifierSQLGeneration.Support old) {
Expand All @@ -358,14 +373,288 @@ private EOQualifierSQLGeneration.Support supportForQualifier(EOQualifier qualifi

@Override
public String sqlStringForSQLExpression(EOQualifier eoqualifier, EOSQLExpression e) {
// Check to see if checking for edge case is enabled
boolean handlesKeyPathWithDerivedAttribute = ERXProperties.booleanForKeyWithDefault(HANDLES_KEY_PATH_WITH_DERIVED_ATTRIBUTE_PROPERTY_NAME, true);
try {
if (handlesKeyPathWithDerivedAttribute && isKeyPathWithDerivedAttributeCase(e.entity(), eoqualifier)) {
return sqlStringForSQLExpressionWithKeyPathWithDerivedAttribute(eoqualifier, e);
}

// Otherwise handle normally
return supportForQualifier(eoqualifier).sqlStringForSQLExpression(eoqualifier, e);
}
catch (JDBCAdaptorException ex) {
ERXExtensions._log.error("Failed to generate sql string for qualifier " + eoqualifier + " on entity " + e.entity() + ".");
if (handlesKeyPathWithDerivedAttribute == false && isKeyPathWithDerivedAttributeCase(e.entity(), eoqualifier)) {
ERXExtensions._log.error("Consider setting " + HANDLES_KEY_PATH_WITH_DERIVED_ATTRIBUTE_PROPERTY_NAME + "=true");
}
throw ex;
}
}

/**
* This method handles an edge case where the key of the qualifier is a key path and
* the last key in the key path references a derived attribute. For example, if the
* key is "order.customerAge" and customerAge were defined as:
*
* (TRUNC(MONTHS_BETWEEN(orderDate,customer.birthDate)/12,0))
*
* and the key value qualifier was something like "order.customerAge > 50" then this
* method would generate SQL like this:
*
* (TRUNC(MONTHS_BETWEEN(T1.ORDER_DATE,T2.BIRTH_DATE)/12,0)) > 50
*
* Without this fix EOF ends up throwing an exception.
*
* @param eoqualifier An EOKeyValueQualifier with a key that is a key path and the last
* component in the key path is a derived attribute.
* @param e The EOSQLExpression participating in the SQL generation.
* @return The SQL for the eoqualifier.
*/
public String sqlStringForSQLExpressionWithKeyPathWithDerivedAttribute(EOQualifier eoqualifier, EOSQLExpression e) {
// Using the example where the key-value qualifier's key is the key path
// 'order.customerAge' and its last key 'customerAge' is an attribute defined
// as '(TRUNC(MONTHS_BETWEEN(orderDate,customer.birthDate)/12,0))'
// then we get the destination attribute, i.e. 'customerAge'. Then we parse
// the properties referenced by its definition, i.e. 'orderDate'
// and 'customer.birthDate'.

EOKeyValueQualifier keyValueQualifier = (EOKeyValueQualifier) eoqualifier;
String keyPath = keyValueQualifier.key();
EOAttribute derivedAttribute = destinationAttribute(e.entity(), keyPath);
NSArray<String> propertyKeys = parseDefinitionPropertyKeys(derivedAttribute);

// Get the keys preceding the derived attribute, for example, if key
// is 'order.customerAge' then the key preceding 'customerAge' would be
// 'order' which will become the prefix.

NSArray<String> allKeys = NSArray.componentsSeparatedByString(keyPath, ".");
String lastKey = allKeys.lastObject();
NSMutableArray<String> prefixKeys = allKeys.mutableClone();
prefixKeys.removeObject(lastKey);
String prefix = prefixKeys.componentsJoinedByString(".") + ".";

// Now we prefix every key path referenced by the definition and then
// generate SQL for them. For example the orderDate property key will
// become order.orderDate and customer.birthDate will become
// order.customer.birthDate. We then convert it to SQL, i.e. T2.ORDER_DATE
// and replace it in the definition. The end result is to have SQL that
// looks like this:
//
// (TRUNC(MONTHS_BETWEEN(T1.ORDER_DATE,T2.BIRTH_DATE)/12,0)) > 60.0
//
String sqlDefinition = derivedAttribute.definition();
for (String unPrefixedKey : propertyKeys) {
String prefixedKey = prefix + unPrefixedKey;
String sqlString = e.sqlStringForAttributeNamed(prefixedKey);
sqlDefinition = sqlDefinition.replaceAll(unPrefixedKey, sqlString);
}

//
// Up to this point we have generated the SQL for the key by replacing
// it for the SQL for its definition and referenced properties. Now
// we need to add the SQL for the selector and value in the qualifier.

//
// Make sure that we have a value before we proceed. If the value
// turns out to be an EOQualifierValue which is a place holder for
// a value then we don't have a value and we should throw an exception.
//

NSSelector qualifierSelector = keyValueQualifier.selector();
Object qualifierValue = keyValueQualifier.value();
if ((qualifierValue instanceof EOQualifierVariable)) {
throw new IllegalStateException(
"sqlStringForKeyValueQualifier: attempt to generate SQL for "
+ eoqualifier.getClass().getName()
+ " " + eoqualifier
+ " failed because the qualifier variable '$"
+ ((EOQualifierVariable)qualifierValue).key()
+ "' is unbound."
);
}

String keyString = sqlDefinition;

boolean isLike =
qualifierSelector.equals(EOQualifier.QualifierOperatorLike)
|| qualifierSelector.equals(EOQualifier.QualifierOperatorCaseInsensitiveLike);

Object value;
if (isLike) {
// Convert the special literal used in like expressions, i.e. * and ?
// into their SQL equivalent % and _
value = e.sqlPatternFromShellPattern((String)qualifierValue);
} else {
value = qualifierValue;
}

String qualifierSQL;
if (qualifierSelector.equals(EOQualifier.QualifierOperatorCaseInsensitiveLike)) {
String valueString = sqlStringForAttributeValue(e, derivedAttribute, value);
qualifierSQL = e.sqlStringForCaseInsensitiveLike(valueString, keyString);
} else {
String valueString = sqlStringForAttributeValue(e, derivedAttribute, value);
String operatorString = e.sqlStringForSelector(qualifierSelector, value);
qualifierSQL = _NSStringUtilities.concat(keyString, " ", operatorString, " ", valueString);
}
if (isLike) {
char escapeChar = e.sqlEscapeChar();
if (escapeChar != 0) {
qualifierSQL = _NSStringUtilities.concat(qualifierSQL, " ESCAPE '" + escapeChar + "'");
}
}

// If debug mode print something like this:
//
// KeyValueQualifierSQLGenerationSupport handled edge case for key-value qualifier with key referencing derived attribute: Order.customerAge
// Key value qualifier: (order.customerAge > 30)
// Key path: order.customerAge
// Attribute customerAge is defined as: TRUNC(MONTHS_BETWEEN(orderDate,customer.birthDate)/12,0)
// SQL generated: TRUNC(MONTHS_BETWEEN(T1.ORDER_DATE,T2.BIRTH_DATE)/12,0) > ?
//
if (ERXExtensions._log.isDebugEnabled()) {
ERXExtensions._log.debug(getClass().getSimpleName()
+ " handled edge case for key-value qualifier with key path"
+ " referencing a derived attribute: "
+ derivedAttribute.entity().name()
+ "."
+ derivedAttribute.name()
);
ERXExtensions._log.debug(" Key value qualifier: "+ keyValueQualifier);
ERXExtensions._log.debug(" Key path: " + keyValueQualifier.key());
ERXExtensions._log.debug(" Attribute " + derivedAttribute.name() + " is defined as: " + derivedAttribute.definition());
ERXExtensions._log.debug(" SQL generated: " + qualifierSQL);
}

return qualifierSQL;
}

/**
* Uses the EOSQLExpression provided to get the SQL string for value and
* corresponding attribute.
*
* @param e The EOSQLExpression to use to generate the SQL
* @param att The attribute corresponding to the value passed in
* @param value The value to convert to SQL
* @return The SQL string for the value
*/
public String sqlStringForAttributeValue(EOSQLExpression e, EOAttribute att, Object value) {
if (value != NSKeyValueCoding.NullValue
&& (((e.useBindVariables()) && (e.shouldUseBindVariableForAttribute(att))) || (e.mustUseBindVariableForAttribute(att)))) {

NSMutableDictionary<String, Object> binding = e.bindVariableDictionaryForAttribute(att, value);
e.addBindVariableDictionary(binding);
return (String)binding.objectForKey("BindVariablePlaceholder");
}
return e.formatValueForAttribute(value, att);
}

public static String formatValueForAttribute(EOSQLExpression e, Object value, EOAttribute attribute) {
return e.formatValueForAttribute(value, attribute);
}

/**
* Normally EOF can handle key value qualifiers with a key corresponding to a
* derived attribute, i.e. fullName attribute defined as firstName || ' ' || lastName.
* However, if the key is a key path, i.e. customer.fullName then EOF throws an
* exception. This method checks to see if the key in the eoqualifier is a key
* path and the last key in the key path corresponds to a derived attribute.
*
* @param entity The entity where the eoqualifier is rooted
* @param eoqualifier A qualifier to test
* @return true if the eoqualifier is an EOKeyValueQualifier and its key is a
* key path (with two or more keys) and the last key references a derived
* attribute.
*/
public static boolean isKeyPathWithDerivedAttributeCase(EOEntity entity, EOQualifier eoqualifier) {
// Make sure it's a EOKeyValueQualifier as we need to get a key
if (!(eoqualifier instanceof EOKeyValueQualifier)) {
return false;
}

EOKeyValueQualifier keyValueQualifier = (EOKeyValueQualifier) eoqualifier;
String keyPath = keyValueQualifier.key();

// If it's not a key path with at least two keys then it's not
// the edge case that we are looking for
if (keyPath.contains(".") == false) {
return false;
}

// Traverse the key path to get to last attribute referenced
EOAttribute attr = destinationAttribute(entity, keyPath);

// If the key path lead to an attribute that is derived then it is
// the special case that we checking for.
return attr != null && attr.isDerived();
}

/**
* Returns the last attribute referenced by key path.
*
* @param rootEntity The entity where the key path begins.
* @param keyPath The key path leading to an attribute.
* @return The attribute referenced by the last key in the key path.
* If the last key in the key path is not an attribute then it returns null.
*/
public static EOAttribute destinationAttribute(EOEntity rootEntity, String keyPath) {
// Parse the keys in key path
String[] keys = keyPath.split("\\.");

// Traverse the key path to get to last attribute referenced
EOAttribute attr = null;
EOEntity entity = rootEntity;
for (String key : keys) {
EORelationship relationship = entity.anyRelationshipNamed(key);
if (relationship != null) {
entity = relationship.destinationEntity();
attr = null;
} else {
attr = entity.anyAttributeNamed(key);
}
}
return attr;
}

/**
* Given the definition of a derived attribute belonging to the entity provided
* this method parses the definition looking for key paths that represent properties.
* For example, a customerAge attribute in a hypothetical Order entity could have a
* definition of 'TRUNC(MONTHS_BETWEEN(orderDate,customer.birthDate)/12,0)' and you
* can call this method to get an array containing orderDate and customer.birthDate
* which are the propertyKeys found in the definition.
*
* @param derivedAttribute An EOAttribute with a definition
* @return An array with the key paths referenced by the definition of the derived
* attribute.
*/
public static NSArray<String> parseDefinitionPropertyKeys(EOAttribute derivedAttribute) {
EOEntity entity = derivedAttribute.entity();
String definition = derivedAttribute.definition();
Pattern p = Pattern.compile("('[^']*')|[,]*+\\b([a-z]+[a-zA-Z0-9_\\.]*)");
Matcher m = p.matcher(definition);
NSMutableArray<String> propertyKeys = new NSMutableArray<String>();
while (m.find()) {
// Please note that the regular expression has two groups separated
// with an or, i.e. |. The first group in the regular expression
// matches a single-quoted literal which are to be skipped because
// the text within it are not to be considered properties.
if (m.group(1) != null) {
continue;
}

// The second group matches key paths preceded optionally with a comma
// as in the customerAge definition example. So get the key path and if
// consider it a property if it references an attribute when applied to
// the entity.
String keyPath = m.group(2);
if (destinationAttribute(entity, keyPath) != null) {
propertyKeys.add(keyPath);
}
}
return propertyKeys.immutableClone();
}

@Override
public EOQualifier schemaBasedQualifierWithRootEntity(EOQualifier eoqualifier, EOEntity eoentity) {
Expand Down

0 comments on commit bee51ac

Please sign in to comment.