diff --git a/Frameworks/Core/ERExtensions/Sources/er/extensions/ERXExtensions.java b/Frameworks/Core/ERExtensions/Sources/er/extensions/ERXExtensions.java
index 4bca5b29f4f..5229d63cb64 100644
--- a/Frameworks/Core/ERExtensions/Sources/er/extensions/ERXExtensions.java
+++ b/Frameworks/Core/ERExtensions/Sources/er/extensions/ERXExtensions.java
@@ -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;
@@ -39,6 +41,7 @@
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;
@@ -46,9 +49,12 @@
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;
@@ -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) {
@@ -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 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 allKeys = NSArray.componentsSeparatedByString(keyPath, ".");
+ String lastKey = allKeys.lastObject();
+ NSMutableArray 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 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 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 propertyKeys = new NSMutableArray();
+ 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) {
diff --git a/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQuery.java b/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQuery.java
new file mode 100644
index 00000000000..744f7bfec26
--- /dev/null
+++ b/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQuery.java
@@ -0,0 +1,1794 @@
+package er.extensions.eof;
+
+import com.webobjects.foundation.*;
+import com.webobjects.jdbcadaptor.JDBCAdaptor;
+import com.webobjects.jdbcadaptor.JDBCPlugIn;
+import com.webobjects.eocontrol.*;
+import com.ibm.icu.text.SimpleDateFormat;
+import com.webobjects.eoaccess.*;
+
+import er.extensions.eof.ERXEC;
+import er.extensions.eof.ERXEOAccessUtilities;
+import er.extensions.eof.ERXEOControlUtilities;
+import er.extensions.eof.ERXKey;
+import er.extensions.eof.ERXModelGroup;
+import er.extensions.eof.ERXQ;
+import er.extensions.eof.ERXEOAccessUtilities.ChannelAction;
+import er.extensions.foundation.ERXProperties;
+import er.extensions.jdbc.ERXSQLHelper;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
ERXQuery.java
+ *
+ *
Overview
+ *
+ *
This class has a fluent API that mimics a select statement:
+ * It allows you to use EOF/Wonder higher-level constructs (qualifiers, attributes,
+ * orderings, key paths, ERXKeys, etc.) to create a query that looks like this:
+ *
+ *
+ * SELECT ...
+ * FROM ...
+ * WHERE ...
+ * GROUP BY ...
+ * HAVING ...
+ * ORDER BY ...
+ *
+ *
+ *
+ *
Specifying the Attributes to Fetch
+ *
+ *
+ * The select() method is very flexible and powerful. It accepts a variable number
+ * of objects of different types that specify the attributes to fetch. These objects
+ * can be EOAttributes, ERXKeys, Strings. You may also specify any Iterable such as
+ * NSArray, List, Collection, etc. containing any combination of these (EOAttributes,
+ * ERXKeys, Strings).
+ *
+ *
+ * The ERXKeys and String objects correspond to keys and key paths to the attributes
+ * to fetch, i.e. "customer.name". The keys and key paths can also be relationships
+ * to objects, i.e. "customer" which translate into a fetch of foreign keys used to
+ * build object faults and return them in the results.
+ *
+ *
+ * You may call the select() method multiple times to keep adding to the list of
+ * attributes to fetch.
+ *
+ *
+ *
Using Ad Hoc Attributes
+ *
+ *
+ * It is very common to aggregate attributes in these queries. For this purpose, you may
+ * want to create what ERXQuery refers to as ad hoc attributes. These attributes have a
+ * definition but are not physically attached to the entity. You can use the
+ * ERXQueryAttributes class to easily create multiple ad hoc attributes. The definition
+ * of the attribute can reference relationships and attributes as shown below. If you
+ * just want to create a single ad hoc attribute you may use the ERXQueryEOAttribute class.
+ *
+ *
+ *
+ * {@code
+ * // Using a single query against the order entity to count the number of
+ * // orders and line items that match an order qualifier.
+ *
+ * ERXQueryAttributes attributes = ERXQueryAttributes.create(orderEntity)
+ * .add("itemCount", "COUNT(DISTINCT lineItems.lineItemID)", "intNumber")
+ * .add("orderCount", "COUNT(DISTINCT orderID)", "intNumber");
+ *
+ * ERXQuery query =
+ * ERXQuery.create()
+ * .select (attributes)
+ * .from (orderEntity)
+ * .where (qualifier);
+ *
+ * // Fetch into a dictionary
+ * NSDictionary row = query.fetch().lastObject();
+ *
+ * int orderCount = ((Number) row.objectForKey("orderCount")).intValue();
+ * int itemCount = ((Number) row.objectForKey("itemCount")).intValue();
+ * }
+ *
+ *
+ *
Fetching Results into a Custom Class
+ *
It is useful to fetch results into objects of a custom class.
+ * This allows you to have type checking on the getter methods and add methods for
+ * computed values on the data fetched. For the example above you could have fetched the
+ * results into a custom class as follows:
+ *
+ * {@code
+ * // Fetch into object instances of the a custom Result class
+ * Result result = query.fetch(editingContext, Result.class).lastObject();
+ * int orderCount = result.orderCount();
+ * int itemCount = result.itemCount();
+ * }
+ *
+ *
The Result custom class would have to be defined as
+ * shown below. The constructor may keep the mutable dictionary passed in to the
+ * constructor or make an immutable copy from it as shown below.
+ *
+ * {@code
+ * public static class Result {
+ * NSDictionary data;
+ *
+ * public Result(EOEditingContext ec, NSMutableDictionary row) {
+ * data = row.immutableClone();
+ * }
+ *
+ * public int itemCount() {
+ * return ((Number) data.objectForKey("itemCount")).intValue();
+ * }
+ * public int orderCount() {
+ * return ((Number) data.objectForKey("orderCount")).intValue();
+ * }
+ * }
+ * }
+ *
+ * In general, fetching into a custom class can be done in several ways:
+ *
+ * {@code
+ * // If your custom class has a constructor that takes an editing context and
+ * // a mutable dictionary then it is very simple:
+ * NSArray objs = query.fetch(editingContext, Foo.class);
+ *
+ * // Using java 8 or later you may use a lambda expression:
+ * NSArray objs = query.fetch(editingContext, (ec, row) -> new Foo(ec, row));
+ *
+ *
+ * // You may also create an implementation of the RecordConstructor
+ * // functional interface and pass it into the fetch method:
+ * ERXQuery.RecordConstructor recordConstructor =
+ * new ERXQuery.RecordConstructor {
+ * @Override
+ * public Foo constructRecord(EOEditingContext ec, NSMutableDictionary row) {
+ * return new Foo(ec, row);
+ * }
+ * };
+ * NSArray objs = query.fetch(editingContext, recordConstructor)
+ * }
+ *
+ *
+ *
Augmenting Row Values
+ *
You can have entries from a dictionary added in to the rows
+ * fetched from the database. The mutable dictionary passed in to the record
+ * constructor will contain the data fetched along with the keys/values from
+ * this recordInitializationValues dictionary.
+ * An alternate way to define your ad hoc attributes is to define them in your entity
+ * and flagging them as non-class properties. Unlike ERXQueryEOAttribute objects,
+ * these attributes will be instances of EOAttribute and reside in your entity. They
+ * may be a bit distracting when looking at the entity if you have a lot but this
+ * method allows you to reuse all the existing attributes and relationships already
+ * defined in the entity and does not require code for creating the attributes.
+ *
+ *
+ * One incovenience is that eogeneration templates do not generate ERXKeys
+ * for non-class properties. However, this problem could be overcome by enhancing
+ * the eogeneration templates to generate ERXKeys for derived non-class property
+ * attributes.
+ * It would be nice to enhance the eogeneration templates to also create a custom
+ * class for fetching the results, i.e. WonderEntitySummary.java and _WonderEntitySummary.java
+ * with the getters for attributes/relationships in the entity including derived non-class
+ * properties. These templates would be used when the entity has a user info key with
+ * ERXQuery.enabled=yes.
+ *
+ *
Limitations
+ *
+ *
+ * Ad hoc attributes created with ERXQueryAttributes or ERXQueryEOAttribute are not
+ * physically attached to an entity. When EOF generates SQL for a qualifier it calls
+ * sqlStringForSQLExpression(q,e) where q is an EOQualifier and e is an EOSQLExpression.
+ * Qualifiers then try to reach the attribute by following the qualifier's referenced
+ * keys starting with the entity of the EOSQLExpression, i.e. e.entity().
+ *
+ *
+ * The current workaround used by ERXQuery is to temporarily add to the entity any
+ * ad hoc attributes referenced by the qualifiers. This typically happens with the
+ * havingQualifier which normally references the ad hoc attributes corresponding to
+ * aggregated attributes. For example, {@code "sumTotalAmount"} defined as
+ * {@code "SUM(totalAmount)"} could be used in a having qualifier:
+ *
+ * {@code
+ * // When grouping orders by customer and fetching sumTotalAmount we may want to have
+ * // this having qualifier so that we only fetch the groups totaling more than 1000.
+ * EOQualifier havingQualifier = ERXQ.greaterThan("sumTotalAmount", new BigDecimal(1000.0));
+ * }
+ *
+ *
+ * However, if you were to define your {@code "sumTotalAmount"} attribute in your entity
+ * as a derived non-class property with definition {@code "SUM(totalAmount)"} then ERXQuery
+ * doesn't have to add the attribute to the entity.
+ *
+ *
+ *
+ *
+ * @author Ricardo J. Parada
+ */
+
+@SuppressWarnings("javadoc")
+
+public class ERXQuery {
+
+ /**
+ * new org.slf4j.Logger
+ */
+ static final Logger log = LoggerFactory.getLogger(ERXQuery.class);
+
+ protected EOEditingContext editingContext;
+ protected EOEntity mainEntity;
+ protected EOQualifier mainSelectQualifier;
+ protected EOQualifier havingQualifier;
+ protected NSMutableArray orderings;
+ protected NSMutableDictionary relationshipAliases;
+
+ protected boolean usesDistinct;
+ protected boolean isCountingStatement;
+ protected String queryHint;
+ protected boolean useBindVariables;
+
+ // These are populated by computeSelectAndGroupingAttributes()
+ protected NSMutableArray fetchKeys;
+ protected NSMutableArray groupingKeys;
+ protected NSMutableArray adHocAttributes;
+ protected NSMutableArray adHocGroupings;
+
+ protected NSMutableArray selectAttributes;
+ protected NSMutableArray groupingAttributes;
+ protected NSMutableDictionary attributesByName;
+ protected NSMutableSet relationshipKeysSet;
+ protected boolean refreshRefetchedObjects;
+
+ protected int serverFetchLimit;
+ protected int clientFetchLimit;
+
+ protected double queryEvaluationTime;
+
+ protected ERXQuery() {
+ // Set defaults
+ fetchKeys = new NSMutableArray();
+ groupingKeys = new NSMutableArray();
+ orderings = new NSMutableArray();
+ refreshRefetchedObjects = false;
+ usesDistinct = false;
+ isCountingStatement = false;
+ queryHint = null;
+ relationshipAliases = new NSMutableDictionary();
+
+
+ // This will hold any ad hoc attributes to be selected
+ adHocAttributes = new NSMutableArray(2);
+ // This will hold any ad hoc attributes to use in the group by clause
+ adHocGroupings = new NSMutableArray(2);
+
+ // Determine features to enable / disable
+ //
+ useBindVariables = ERXProperties.booleanForKeyWithDefault("er.extensions.eof.ERXQuery.useBindVariables", false);
+ }
+
+ //
+ // FLUENT API
+ //
+
+ public static ERXQuery create() {
+ return new ERXQuery();
+ }
+
+ /**
+ * Specifies whether to select count(*)
+ */
+ public ERXQuery selectCount() {
+ isCountingStatement = true;
+ return this;
+ }
+
+ /**
+ * Specifies the attributes to fetch. The attributes may be specified using
+ * EOAttributes, ERXKeys, String objects (for keys and key paths), or Iterable
+ * objects such as NSArray, List, Collection containing EOAttributes, ERXKeys,
+ * Strings or inclusive other Iterables. The String and ERXKey objects must
+ * correspond to the names of the attributes to fetch or to the key paths to
+ * leading to the attributes to fetch. You may call the select() method
+ * multiple times to keep adding to the list of attributes to fetch.
+ */
+ public ERXQuery select(Object... attributesOrKeys) {
+ for (Object obj : attributesOrKeys) {
+ if (obj instanceof String) {
+ String key = (String) obj;
+ fetchKeys.add(key);
+ } else if (obj instanceof ERXKey>) {
+ ERXKey> erxKey = (ERXKey>) obj;
+ String key = erxKey.key();
+ fetchKeys.add(key);
+ } else if (obj instanceof EOAttribute) {
+ EOAttribute adHocAttribute = (EOAttribute) obj;
+ adHocAttributes.add(adHocAttribute);
+ } else if (obj instanceof Iterable) {
+ Iterable iterable = (Iterable) obj;
+ // Use recursion to add each object in the array
+ for (Object e : iterable) {
+ select(e);
+ }
+ }
+ }
+ return this;
+ }
+
+
+ /**
+ * Specifies whether or not to use DISTINCT.
+ */
+ public ERXQuery usingDistinct() {
+ usesDistinct = true;
+ return this;
+ }
+
+ /**
+ * Specifies whether to refresh refetched objects referenced
+ * by relationship keys, i.e. "customer".
+ */
+ public ERXQuery refreshingRefetchedObjects() {
+ refreshRefetchedObjects = true;
+ return this;
+ }
+
+ /**
+ * Specifies the EOEntity object to select from.
+ */
+ public ERXQuery from(EOEntity entity) {
+ mainEntity = entity;
+ return this;
+ }
+
+ /**
+ * Specifies the name of EOEntity object to select from.
+ */
+ public ERXQuery from(String entityName) {
+ return from(ERXModelGroup.defaultGroup().entityNamed(entityName));
+ }
+
+ /**
+ * Specifies the main qualifier used to build the where clause.
+ */
+ public ERXQuery where(EOQualifier qual) {
+ mainSelectQualifier = mainEntity.schemaBasedQualifier(qual);
+ return this;
+ }
+
+ /**
+ * Use this to specify the attributes to group by. The objects can be EOAttributes,
+ * ERXKeys, Strings, or any Iterable such as NSArrays, Lists, Collections, etc. containing
+ * EOAttributes, ERXKeys, Strings or inclusive other Iterables. The ERXKeys and String
+ * objects must correspond to the keys or key paths to the attributes to group by. You may
+ * call this method multiple times to keep on adding to the list of attributes to group by.
+ */
+ public ERXQuery groupBy(Object... attributesOrKeys) {
+ for (Object obj : attributesOrKeys) {
+ if (obj instanceof String) {
+ String key = (String) obj;
+ groupingKeys.add(key);
+ } else if (obj instanceof ERXKey>) {
+ ERXKey> erxKey = (ERXKey>) obj;
+ String key = erxKey.key();
+ groupingKeys.add(key);
+ } else if (obj instanceof EOAttribute) {
+ EOAttribute adHocAttribute = (EOAttribute) obj;
+ groupingAttributes.add(adHocAttribute);
+ } else if (obj instanceof Iterable) {
+ Iterable iterable = (Iterable) obj;
+ for (Object e : iterable) {
+ groupBy(e);
+ }
+ } else {
+ throw new RuntimeException(getClass().getSimpleName()
+ + "'s groupBy() does not accept instances of "
+ + obj.getClass().getName());
+ }
+ }
+ return this;
+ }
+
+
+ /**
+ * Specifies the sort orderings used to build the order by clause. The objects passed
+ * in can be EOSortOrderings or Iterables containing EOSortOrderings or other Iterables.
+ * You may call this method multiple times to keep adding to the list of orderings.
+ */
+ public ERXQuery orderBy(Object... orderingObjects) {
+ for (Object obj : orderingObjects) {
+ if (obj instanceof EOSortOrdering) {
+ EOSortOrdering sortOrdering = (EOSortOrdering) obj;
+ orderings.add(sortOrdering);
+ } else if (obj instanceof Iterable) {
+ Iterable iterable = (Iterable) obj;
+ for (Object o : iterable) {
+ orderBy(o);
+ }
+ } else {
+ throw new RuntimeException(getClass().getSimpleName()
+ + "'s orderBy() does not accept instances of "
+ + obj.getClass().getName());
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Specifies the qualifier for the having clause.
+ */
+ public ERXQuery having(EOQualifier qual) {
+ havingQualifier = mainEntity.schemaBasedQualifier(qual);
+ return this;
+ }
+
+ /**
+ * This string is inserted after the SELECT keyword in the generated SQL
+ * padded with a space on both sides. This allows you to send a hint
+ * to the database server.
+ */
+ public ERXQuery usingQueryHint(String value) {
+ queryHint = value;
+ return this;
+ }
+
+ /**
+ * Enables use of bind variables. If this is not called then
+ * ERXQuery looks at the er.extensions.eof.ERXQuery.useBindVariables property
+ * which defaults to false currently, thereby placing values in-line
+ * with the SQL generated.
+ */
+ public ERXQuery usingBindVariables() {
+ useBindVariables = true;
+ return this;
+ }
+
+ /**
+ * If specified then the query will be wrapped with something like
+ * this, depending on the database product:
+ *
+ * SELECT * FROM ( query ) WHERE ROWNUM <= limit
+ */
+ public ERXQuery serverFetchLimit(int limit) {
+ this.serverFetchLimit = limit;
+ return this;
+ }
+
+ /**
+ * If specified then the fetch will be stopped/canceled after fetching
+ * clientFetchLimit records. This does not affect the SQL generated unlike
+ * serverFetchLimit.
+ */
+ public ERXQuery clientFetchLimit(int limit) {
+ this.clientFetchLimit = limit;
+ return this;
+ }
+
+ //
+ // Fetch methods
+ //
+
+ public NSArray> fetch() {
+ NSDictionary recordInitializationValues = NSDictionary.emptyDictionary();
+ EOEditingContext ec = ERXEC.newEditingContext();
+ return fetch(ec, recordInitializationValues);
+ }
+
+ public NSArray> fetch(EOEditingContext ec) {
+ NSDictionary recordInitializationValues = NSDictionary.emptyDictionary();
+ return fetch(ec, recordInitializationValues);
+ }
+
+ public NSArray> fetch(EOEditingContext ec, NSDictionary recordInitializationValues) {
+ return fetch(getExpression(ec), selectAttributes, ec, recordInitializationValues, new DefaultRecordConstructor());
+ }
+
+ /**
+ * Returns fetch(ec, recordClass, NSDictionary.emptyDictionary())
+ */
+ public NSArray fetch(EOEditingContext ec, Class recordClass) {
+ NSDictionary recordInitializationValues = NSDictionary.emptyDictionary();
+ return fetch(ec, recordInitializationValues, recordClass);
+ }
+
+ /**
+ * Use this method to fetch results into objects of the specified class. The class
+ * must have a constructor that takes an EOEditingContext and an NSMutableDictionary
+ * as arguments. The row passed into the constructor will contain data fetched for the
+ * row as well as the entries from recordInitializatonValues dictionary.
+ */
+ public NSArray fetch(EOEditingContext anEC, final NSDictionary recordInitializationValues, Class recordClass) {
+ // Get the constructor once here before we enter the fetch-loop
+ final Constructor constructor;
+ try {
+ Class>[] parameterTypes = new Class>[] { EOEditingContext.class, NSMutableDictionary.class };
+ constructor = recordClass.getConstructor(parameterTypes);
+ } catch (NoSuchMethodException e) {
+ throw new NSForwardException(e, "ERXQuery: record class '" + recordClass.getName() + "' must have a constructor with an EOEditingContext and NSMutableDictionary as arguments");
+ } catch (SecurityException e) {
+ throw new NSForwardException(e);
+ }
+ // We got the constructor for recordClass above... Now let's use it
+ // to create a RecordConstructor implementation that calls it to
+ // create instances for the recordClass class.
+ RecordConstructor recordConstructor =
+ new RecordConstructor() {
+ @Override
+ public T constructRecord(EOEditingContext ec, NSMutableDictionary row) {
+ try {
+ Object[] args = new Object[] { ec, row };
+ return constructor.newInstance(args);
+ } catch (InstantiationException exception) {
+ throw new RuntimeException(exception);
+ } catch (IllegalAccessException exception) {
+ throw new RuntimeException(exception);
+ } catch (InvocationTargetException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+ };
+
+ return fetch(anEC, recordInitializationValues, recordConstructor);
+ }
+
+ /**
+ *
+ * Use this method to fetch either by using an implementation of the RecordConstructor
+ * functional interface or by using a lambda expression as follows:
+ *
+ */
+ public NSArray fetch(EOEditingContext ec, NSDictionary recordInitializationValues, RecordConstructor recordConstructor) {
+ EOSQLExpression sqlExpression = getExpression(ec);
+ return fetch(sqlExpression, selectAttributes, ec, recordInitializationValues, recordConstructor);
+ }
+
+ /**
+ * Convenience method to return fetch(ec, NSDictionary.emptyDictionary(), recordConstructor)
+ */
+ public NSArray fetch(EOEditingContext ec, RecordConstructor recordConstructor) {
+ NSDictionary recordInitializationValues = NSDictionary.emptyDictionary();
+ return fetch(ec, recordInitializationValues, recordConstructor);
+ }
+
+ /**
+ * Core fetch method. Given the EOSQLExpression built to fetch the selectAttributes
+ * this method fetches the results. As each row result is fetched this method calls
+ * the recordConstructor with the editing context and mutable dictionary containing
+ * the row values and the entries from the initValues dictionary. The record
+ * constructor should return an instance of T. The T instances are then placed
+ * into an array that this method returns, i.e. {@code NSArray}
+ */
+ protected NSArray fetch(
+ final EOSQLExpression expression,
+ final NSArray fetchAttributes,
+ final EOEditingContext ec,
+ final NSDictionary initValues,
+ final RecordConstructor recordConstructor
+ )
+ {
+ // Array to hold fetched records
+ final NSMutableArray records = new NSMutableArray();
+
+ // Create channel action anonymous class for evaluating SQL and fetching records
+ ChannelAction action = new ERXEOAccessUtilities.ChannelAction() {
+ @Override
+ protected int doPerform(EOAdaptorChannel channel) {
+ // Record starting time
+ long start = new NSTimestamp().getTime();
+ channel.evaluateExpression(expression);
+
+ // Compute elapsed time
+ long end = new NSTimestamp().getTime();
+ queryEvaluationTime = (end - start) / 1000.0;
+
+ // Log elapsed time
+ if (log.isDebugEnabled()) {
+ log.debug("Expression evaluation time = "
+ + queryEvaluationTime + " seconds.\n\n");
+ }
+ // Use the names of the fetch attributes for the keys in the
+ // row dictionaries when fetching
+ setupAdaptorChannelEOAttributes(channel, fetchAttributes);
+
+ // Fetch results
+ try {
+ boolean hasInitValues = initValues.count() > 0;
+ boolean removesForeignKeysFromRowValues = ERXProperties.booleanForKeyWithDefault("er.extensions.eof.ERXQuery.removesForeignKeysFromRowValues", true);
+ NSMutableDictionary row = channel.fetchRow();
+ while (row != null) {
+ // Replace any foreign keys with their corresponding relationship keys
+ // and the enterprise object as the value.
+ for (RelationshipKeyInfo relKeyInfo : relationshipKeysSet.allObjects()) {
+ Object eo = null;
+ String entityName = relKeyInfo.entityName();
+ String relationshipKey = relKeyInfo.relationshipKeyPath();
+ String foreignKey = relKeyInfo.sourceAttributeKeyPath();
+ Object primaryKeyValue = row.objectForKey(foreignKey);
+ if (primaryKeyValue != NSKeyValueCoding.NullValue) {
+ eo = ERXEOControlUtilities.objectWithPrimaryKeyValue(ec, entityName, primaryKeyValue, null, refreshRefetchedObjects);
+ row.setObjectForKey(eo, relationshipKey);
+ }
+ if (removesForeignKeysFromRowValues) {
+ row.removeObjectForKey(foreignKey);
+ }
+ }
+ if (hasInitValues) {
+ row.addEntriesFromDictionary(initValues);
+ }
+ T obj = recordConstructor.constructRecord(ec, row);
+ records.addObject(obj);
+ // If a fetch limit was specified then exit fetch-loop as soon
+ // as the limit is reached
+ if (clientFetchLimit > 0 && records.count() >= clientFetchLimit) {
+ break;
+ }
+ row = channel.fetchRow();
+ }
+ } catch (Throwable t) {
+ log.error("Error occurred while fetching rows: ", t);
+ throw new RuntimeException(t);
+ } finally {
+ channel.cancelFetch();
+ }
+ return records.count();
+ }
+ };
+
+ // Perform the action to evaluate the SQL and fetch the records
+ action.perform(ec, expression.entity().model().name());
+ return records;
+ }
+
+ protected static void setupAdaptorChannelEOAttributes(EOAdaptorChannel adaptorChannel, NSArray selectAttributes) {
+ // Have the adaptor provide the attributes to fetch the results. These attributes
+ // have weird names. Here we borrow a technique from David Scheck shared on the
+ // webobjects mailing list. The technique consists in renaming the attributes
+ // provided by the adaptor channel and name it the same as the corresponding
+ // attribute used by ERXQuery. The attribute names used by ERXQuery are more
+ // meaningful and what the developer expects to see in the row dictionary
+ // passed in to the record constructor.
+ if (selectAttributes != null && selectAttributes.count() > 0) {
+
+ // Rename the EOAttributes provided by the adaptor
+ NSArray adaptorSelectAttributes = adaptorChannel.describeResults();
+ int count = adaptorSelectAttributes.count();
+ for (int i = 0; i < count; i++) {
+ EOAttribute adaptorSelectAttribute = adaptorSelectAttributes.objectAtIndex(i);
+ EOAttribute selectAttribute = selectAttributes.objectAtIndex(i);
+ adaptorSelectAttribute.setName(selectAttribute.name());
+ String externalType = selectAttribute.externalType();
+ String className = selectAttribute.className();
+ String valueType = selectAttribute.valueType();
+ if (externalType != null) adaptorSelectAttribute.setExternalType(externalType);
+ if (className != null) adaptorSelectAttribute.setClassName(className);
+ if (valueType != null) adaptorSelectAttribute.setValueType(valueType);
+ }
+ adaptorChannel.setAttributesToFetch(adaptorSelectAttributes);
+ }
+ }
+
+
+ /**
+ * Sets the table alias to use for a given relationship name. For example, if
+ * the query selects from CLAIM and the main query qualifier joins to other tables
+ * including the LINE_ITEM table via the lineItems relationship. If you don't
+ * specify a table alias for the lineItems relationship then it would use whatever
+ * EOF comes up with, for example T3. If you wanted X1 to be used as the table
+ * alias then simply call this method with "lineItems" as the relationship name
+ * and "X1" as the table alias.
+ */
+ public ERXQuery usingRelationshipAlias(String relationshipName, String alias) {
+ relationshipAliases.setObjectForKey(alias, relationshipName);
+ return this;
+ }
+
+
+ //
+ // HELPER METHODS
+ //
+
+ /**
+ * Returns a complete select expression as follows:
+ *
+ * SELECT ...
+ * FROM ...
+ * WHERE ...
+ * GROUP BY ...
+ * HAVING ...
+ * ORDER BY ...
+ *
+ *
+ * The {@code WHERE} clause is constructed from the qualifier (if any) passed
+ * into the {@code where()} method and any required joins necessary to access
+ * any referenced properties.
+ *
+ *
+ *
+ * The {@code GROUP BY} clause is constructed from the grouping attributes
+ * specified by calling any of the {@code groupBy()} methods.
+ *
+ *
+ *
+ * The {@code HAVING} clause is constructed if a qualifier is specified by
+ * calling the {@code having()} method.
+ *
+ *
+ *
+ * The {@code ORDER BY} clause is constructed from attributes passed in to
+ * the {@code orderBy()} method.
+ *
+ */
+ public EOSQLExpression getExpression(EOEditingContext ec) {
+ // Establish the editing context. This is important as some of the
+ // helper methods assume the editingContext i-var has been set. For
+ // example EOSQLExpression factory)
+ if (ec != null) {
+ editingContext = ec;
+ } else {
+ editingContext = ERXEC.newEditingContext();
+ }
+
+ // Populate the selectAttributes and groupingAttributes arrays
+ computeSelectAndGroupingAttributes();
+
+ // Incorporate any entity restricting qualifiers (if any) into the mainSelectQualifier
+ if (ERXProperties.booleanForKeyWithDefault("er.extensions.eof.ERXQuery.useEntityRestrictingQualifiers", true)) {
+ // Get expression similar to what will be used to build the SQL
+ EOQualifier restrictingQualifierForReferencedEntities = restrictingQualifierForReferencedEntities();
+ if (restrictingQualifierForReferencedEntities != null) {
+ mainSelectQualifier = ERXQ.and(mainSelectQualifier, restrictingQualifierForReferencedEntities);
+ }
+ }
+
+ // Get initial expression that will be used to build the sql string. Notice that
+ // the sort orderings is null. This avoids an exception when orderings includes
+ // ad hoc attributes that are not physically attached to an entity. We'll build
+ // the GROUP BY clause later in a different manner.
+ EOFetchSpecification spec = new EOFetchSpecification(mainEntity.name(), mainSelectQualifier, null /* orderings */);
+
+ EOSQLExpressionFactory factory = ERXQuery.sqlExpressionFactory(mainEntity, ec);
+ EOSQLExpression e = factory.selectStatementForAttributes(selectAttributes, false, spec, mainEntity);
+
+ // Start building the SELECT... FROM ...
+ StringBuilder sql = new StringBuilder();
+ sql.append("SELECT ");
+ if (queryHint != null) {
+ sql.append(" " + queryHint + " ");
+ }
+ if (usesDistinct && !isCountingStatement) {
+ sql.append("DISTINCT ");
+ }
+
+ if (isCountingStatement) {
+ NSArray pKeyNames = mainEntity.primaryKeyAttributeNames();
+ if (usesDistinct && pKeyNames.count() == 1) {
+ EOAttribute attribute = mainEntity.attributeNamed(pKeyNames.lastObject());
+ sql.append("COUNT(DISTINCT t0." + attribute.columnName() + " )");
+ } else {
+ sql.append("COUNT(*)");
+ }
+
+ } else {
+ sql.append(e.listString());
+ }
+ sql.append("\n");
+ sql.append("FROM ");
+ sql.append(e.tableListWithRootEntity(mainEntity));
+ sql.append("\n");
+
+ // Add WHERE clause if necessary
+ String joinClauseString = e.joinClauseString();
+ String qualClauseString = e.whereClauseString();
+ boolean hasJoinClause = (joinClauseString != null && joinClauseString.length() > 0);
+ boolean hasQualClause = (qualClauseString != null && qualClauseString.length() > 0);
+ boolean hasWhereClause = (hasJoinClause || hasQualClause);
+
+ if (hasWhereClause) {
+ sql.append("WHERE \n\t");
+ }
+
+ if (hasJoinClause) {
+ sql.append(joinClauseString);
+ }
+
+ if (hasQualClause) {
+ if (hasJoinClause) {
+ sql.append("\n\tAND ");
+ }
+ sql.append(qualClauseString);
+ }
+
+ // Append GROUP BY clause
+
+ if (groupingAttributes.count() > 0) {
+ sql.append("\n");
+ sql.append("GROUP BY");
+ sql.append("\n\t");
+ // Add the sql for each grouping attribute
+ Enumeration enumeration = groupingAttributes.objectEnumerator();
+ while (enumeration.hasMoreElements()) {
+ EOAttribute a = enumeration.nextElement();
+ sql.append(sqlStringForAttribute(e, a));
+ if (enumeration.hasMoreElements()) {
+ sql.append(", ");
+ }
+ }
+ }
+
+ // Append HAVING clause
+
+ EOSQLExpression havingExpression = null;
+ if (havingQualifier != null) {
+ // Add any ad hoc attributes referenced by the havingQualifier to mainEntity
+ // so that we can generate the SQL for the having qualifier otherwise EOF
+ // will throw an exception on us when it calls sqlStringForSQLExpression(q,e)
+ // on each qualifier in the qualifier graph and one of them cannot get to
+ // the attribute by looking up q's key in the e.entity().
+
+ // First determine which keys in the havingQualifier reference ad hoc attributes
+ NSSet havingQualifierKeys = havingQualifier.allQualifierKeys();
+ NSMutableArray toBeAdded = new NSMutableArray();
+ for (String aKey : havingQualifierKeys) {
+ EOAttribute a = attributesByName.objectForKey(aKey);
+ if (a instanceof ERXQueryEOAttribute) {
+ toBeAdded.add(a);
+ }
+ }
+
+ // Modify entity by running an anonymous EntityModificationAction
+ new EntityModificationAction() {
+
+ @Override
+ protected void modifyEntity(EOEntity entity) {
+ if (toBeAdded.count() == 0) {
+ return;
+ }
+ // Remember current class properties
+ NSArray classProperties = mainEntity.classProperties();
+
+ // Add the attributes
+ for (EOAttribute a : toBeAdded) {
+ entity.addAttribute(a);
+ }
+
+ // The attributes added are all ad hoc attributes and
+ // we don't want them as class properties. Therefore,
+ // restore original class properties.
+ entity.setClassProperties(classProperties);
+ }
+ }.run(editingContext, mainEntity);
+
+ // The attributes toBeAdded have been added
+ NSMutableArray addedAttributes = toBeAdded;
+
+
+ // Now create an expression to generate SQL for the HAVING clause
+ try {
+ EOFetchSpecification havingSpec = new EOFetchSpecification(mainEntity.name(), havingQualifier, null);
+ havingExpression = factory.selectStatementForAttributes(selectAttributes, false, havingSpec, mainEntity);
+ } catch (Throwable t) {
+ throw new RuntimeException("Error generating SQL for havingQualifier: " + havingQualifier, t);
+ } finally {
+ new EntityModificationAction() {
+
+ @Override
+ protected void modifyEntity(EOEntity entity) {
+ // Remove any attributes that were added to the entity
+ for (EOAttribute a : addedAttributes) {
+ entity.removeAttribute(a);
+ }
+ }
+
+ }.run(editingContext, mainEntity);
+
+ }
+
+ // Append HAVING clause
+ sql.append("\n");
+ sql.append("HAVING");
+ sql.append("\n\t");
+ sql.append(havingExpression.whereClauseString());
+ }
+
+ // Append ORDER BY clause
+
+ /*
+
+ // This was the original code but it has a problem with ad hoc attributes where
+ // EOF throws an exception saying that attribute for key path is not reachable from
+ // the entity. That happens because ad hoc attributes are not physically attached
+ // to the entity. You cannot get to the attribute by looking up the ordering's key
+ // in the entity, which is what EOF does. We do this differently, i.e. look for the
+ // EOAttribute in the select attributes that matches the ordering key. Then we
+ // use that attribute to generate the SQL for it.
+
+ String orderByString = e.orderByString();
+ if (orderByString != null && orderByString.length() > 0) {
+ sql.append("\n");
+ sql.append("ORDER BY");
+ sql.append("\n\t");
+ sql.append(orderByString);
+ }
+
+ */
+
+ if (orderings.count() > 0) {
+ sql.append("\n");
+ sql.append("ORDER BY");
+ sql.append("\n\t");
+ // Add the sql for each ordering attribute
+ Enumeration orderingsEnumeration = orderings.objectEnumerator();
+ while (orderingsEnumeration.hasMoreElements()) {
+ EOSortOrdering ordering = orderingsEnumeration.nextElement();
+ String orderingKey = ordering.key();
+ EOAttribute orderingAttribute = null;
+ for (EOAttribute a : selectAttributes) {
+ if (orderingKey.equals(a.name())) {
+ orderingAttribute = a;
+ break;
+ }
+ }
+ // Append the SQL for the ordering attribute
+ sql.append(sqlStringForOrderingAttribute(e, orderingAttribute, ordering.selector()));
+
+ if (orderingsEnumeration.hasMoreElements()) {
+ sql.append(",\n\t");
+ }
+ }
+ }
+
+ // At this point the sql string is almost complete. We just need to replace
+ // table aliases by the ones desired by the caller. For example, if T3 was
+ // used for the lineItems relationship and the caller wants X1 to be used
+ // instead then we need to replace all occurrences of T3 by X1.
+
+ String sqlString = sql.toString();
+
+ if (relationshipAliases.count() > 0) {
+ // From the WebObjects documentation:
+
+ // aliasesByRelationshipPath() returns a dictionary of table aliases.
+ // The keys of the dictionary are relationship paths -- "department" and
+ // "department.location", for example. The values are the table aliases
+ // for the corresponding table -- "t1" and "t2", for example. The dictionary
+ // always has at least one entry: an entry for the EOSQLExpression's entity.
+ // The key of this entry is the empty string ("") and the value is "t0". The
+ // dictionary returned from this method is built up over time with successive
+ // calls to sqlStringForAttributePath.
+
+ NSDictionary aliasesByRelationshipPath = e.aliasesByRelationshipPath();
+ for (String relationshipPath : relationshipAliases.allKeys()) {
+ String aliasUsed = aliasesByRelationshipPath.objectForKey(relationshipPath);
+ String aliasDesired = relationshipAliases.objectForKey(relationshipPath);
+ sqlString = sqlString.replaceAll("\\b" + aliasUsed, aliasDesired);
+ }
+ }
+
+ // Now build the new expression using the SQL string built from the first
+ // expression and copy the bindings over from the first expression
+ EOSQLExpression mainExpression = factory.expressionForEntity(mainEntity);
+ mainExpression.setStatement(sqlString);
+
+ NSArray> bindVariableDictionaries = e.bindVariableDictionaries();
+ for (NSDictionary binding : bindVariableDictionaries) {
+ mainExpression.addBindVariableDictionary(binding);
+ }
+
+ // Copy the bindings from the havingExpression
+ if (havingExpression != null) {
+ bindVariableDictionaries = havingExpression.bindVariableDictionaries();
+ for (NSDictionary binding : bindVariableDictionaries) {
+ mainExpression.addBindVariableDictionary(binding);
+ }
+ }
+
+ // If we should not use bind variables then replace the bind variable
+ // place holders in the SQL by their values
+ if (!useBindVariables) {
+ String sqlWithBindingsInline = sqlWithBindingsInline(mainExpression.statement(), mainExpression);
+ mainExpression = factory.expressionForEntity(mainEntity);
+ mainExpression.setStatement(sqlWithBindingsInline);
+ }
+
+ // See if you have to add SELECT * FROM ( original select SQL ) WHERE ROWNUM <= fetchLimit
+ if (serverFetchLimit > 0) {
+ String statementWithLimitClause = addLimitClause(mainEntity, mainExpression.statement(), serverFetchLimit);
+ mainExpression.setStatement(statementWithLimitClause);
+ }
+
+ return mainExpression;
+ }
+
+ /**
+ * Turns the SQL statement into something like this:
+ *
+ * "SELECT * FROM ( " + statement + " ) WHERE ROWNUM <= " + limit;
+ *
+ */
+ protected String addLimitClause(EOEntity entity, String statement, int limit) {
+ // This works for ORACLE and I think it is better for my needs that what ERXSQLHelper does.
+ if ("oracle".equals(databaseProductName(entity))) {
+ return wrapped("SELECT * FROM ( ", statement, " ) WHERE ROWNUM <= " + limit);
+ }
+ // Use ERXSQLHelper for all the other database products
+ ERXSQLHelper sqlHelper = ERXSQLHelper.newSQLHelper(entity);
+ return sqlHelper.limitExpressionForSQL(null, null, statement, 0, limit);
+ }
+
+ protected String wrapped(String leftHandSide, String statement, String rightHandSide) {
+ String sql = statement;
+
+ // Assumes each clause in its own line
+ NSArray lines = NSArray.componentsSeparatedByString(sql, "\n");
+ return leftHandSide + "\n " + lines.componentsJoinedByString("\n ") + "\n" + rightHandSide;
+ }
+ /**
+ * Returns the array containing the EOAttributes that ERXQuery used to
+ * fetch the results. This must be called after the results have been
+ * fetched or after calling the getExpression(editingContext) method.
+ * These attributes normally includes attributes specified by the
+ * select() or the groupBy() methods.
+ */
+ public NSArray selectAttributes() {
+ return selectAttributes;
+ }
+
+
+
+ //
+ // HELPER PRIVATE METHODS
+ //
+
+ protected void computeSelectAndGroupingAttributes() {
+ // Initialize arrays for storing the select attributes,
+ // grouping attributes and sort orderings
+ selectAttributes = new NSMutableArray(20);
+ groupingAttributes = new NSMutableArray(20);
+
+ // This keeps track of EOAttribute objects used
+ attributesByName = new NSMutableDictionary();
+
+ // This is a set of RelationshipKeyInfo objects that keeps track
+ // of fetch keys encountered that correspond to relationships,
+ // i.e. "customer.shippingAddress". The RelationshipKeyInfo stores
+ // the relationship key path, i.e. "customer.shippingAddress" as the
+ // foreign key path, i.e. "customer.shippingAddressID" and the
+ // entity of the destination enterprise object.
+ relationshipKeysSet = new NSMutableSet();
+
+ // Add attributes to select
+ for (EOAttribute a : adHocAttributes) {
+ selectAttributes.addObject(a);
+ // Keep track of which ones we've used
+ attributesByName.setObjectForKey(a, a.name());
+ }
+ // Keep track of attributes to group by
+ for (EOAttribute a : adHocGroupings) {
+ groupingAttributes.add(a);
+ // Keep track of which ones we've used
+ attributesByName.setObjectForKey(a, a.name());
+ }
+
+ // Get the EOAttributes for the grouping keys
+ for (String key : groupingKeys) {
+ EOAttribute eoattribute = existingOrNewAttributeForKey(key);
+ groupingAttributes.addObject(eoattribute);
+ selectAttributes.addObject(eoattribute);
+ } /* for (key : groupings) */
+
+ // Make sure orderings are select attributes
+ for (EOSortOrdering ordering : orderings) {
+ String orderingKey = ordering.key();
+ EOAttribute orderingAttribute = null;
+ for (EOAttribute a : selectAttributes) {
+ if (orderingKey.equals(a.name())) {
+ orderingAttribute = a;
+ break;
+ }
+ }
+ // If no attribute for this ordering key then create one
+ // and add it to the select attributes
+ if (orderingAttribute == null) {
+ orderingAttribute = existingOrNewAttributeForKey(orderingKey);
+ selectAttributes.add(orderingAttribute);
+
+ // Now that we added it to the select attributes we have to check
+ // to see if it also needs to be added to the groupingAttributes.
+ // For example, if we are grouping by "patient" and ordering key
+ // is "patient.lastName" then we need to add it to the grouping
+ // attributes in order to generate correct SQL.
+ for (String gKey : groupingKeys) {
+ // Example: if ordering key is "patient.lastName" and grouping key is "patient"
+ if (orderingKey.length() > gKey.length() && orderingKey.startsWith(gKey)) {
+ groupingAttributes.add(orderingAttribute);
+ }
+ }
+ }
+ }
+ // Get the EOAttributes for the keys to fetch
+ for (String key : fetchKeys) {
+ EOAttribute eoattribute = existingOrNewAttributeForKey(key);
+ if (selectAttributes.containsObject(eoattribute) == false) {
+ selectAttributes.addObject(eoattribute);
+ }
+ } /* for (key : columns) */
+
+ // If building a counting statement then there are no select attributes but we need to have
+ // at least one select attribute in order to build an EOSQLExpression otherwise the
+ // EOSQLExpressionFactory method selectStatementForAttributes() throws an exception.
+ if (selectAttributes.count() == 0 && isCountingStatement) {
+ selectAttributes = new NSMutableArray(mainEntity.primaryKeyAttributes());
+ }
+ }
+
+ /**
+ * Inner class to keep track of relationship keys. Relationships keys
+ * are key paths where all the keys are relationships, i.e. order.customer.
+ * ERXQuery fetches the foreign keys and then creates object faults that it
+ * adds automatically to the row dictionary that it passes in to the record
+ * constructor.
+ */
+ private static class RelationshipKeyInfo {
+ private String _entityName;
+ private String _relationshipKeyPath;
+ private String _sourceAttributeKeyPath;
+
+ public RelationshipKeyInfo(String relationshipKeyPath, String sourceAttributeKeyPath, EOEntity entity) {
+ this._relationshipKeyPath = relationshipKeyPath;
+ this._sourceAttributeKeyPath = sourceAttributeKeyPath;
+ this._entityName = entity.name();
+ }
+
+ public String entityName() {
+ return _entityName;
+ }
+ public String relationshipKeyPath() {
+ return _relationshipKeyPath;
+ }
+ public String sourceAttributeKeyPath() {
+ return _sourceAttributeKeyPath;
+ }
+ @Override
+ public int hashCode() {
+ return (_entityName + _relationshipKeyPath + _sourceAttributeKeyPath).hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof RelationshipKeyInfo) {
+ RelationshipKeyInfo relKeyInfo = (RelationshipKeyInfo) obj;
+ return relKeyInfo == this ||
+ ( relKeyInfo.entityName().equals(_entityName)
+ && relKeyInfo.relationshipKeyPath().equals(_relationshipKeyPath)
+ && relKeyInfo.sourceAttributeKeyPath().equals(_sourceAttributeKeyPath)
+ );
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Called by getExpression() to get the attribute or create an
+ * ad-hoc attribute for a key. The keyPath can be relationship key path,
+ * i.e. "customer.shippingAddress". This method keeps track of ad hoc
+ * attributes created so that if it gets called with the same key path
+ * it returns the previously created attribute.
+ */
+ protected EOAttribute existingOrNewAttributeForKey(String keyPath) {
+ // ERXQuery.destinationProperty() below returns the property corresponding
+ // to the last component in the key path. This could be either an EOAttribute
+ // or an EORelationship. For example, "customer.shippingAddress" returns the
+ // shippingAddress relationship. On the other hand, a key path of "customer.birthDate"
+ // would return the birthDate EOAttribute from the Customer entity.
+ EOProperty eoproperty = ERXQuery.destinationProperty(mainEntity, keyPath);
+ if (eoproperty == null) {
+ throw unknownPropertyException(keyPath);
+ }
+ // Parse the keys in the key path
+ NSMutableArray keys = new NSMutableArray(keyPath.split("\\."));
+
+ // If destination property is an EOAttribute
+ if (eoproperty instanceof EOAttribute) {
+ EOAttribute eoattribute = null;
+ if (keys.count() == 1) {
+ // If key path contains a single key, i.e. "birthDate" then the attribute is
+ // the destination property, which is just the existing attribute on
+ // the main entity.
+ eoattribute = (EOAttribute) eoproperty;
+ attributesByName.setObjectForKey(eoattribute, eoattribute.name());
+ } else {
+ // The key path has multiple keys, i.e. "patient.birthDate", so let's create
+ // an ad hoc attribute to reach the destination attribute.
+ String attributeName = keyPath;
+ String definition = keyPath;
+ EOAttribute destinationAttribute = (EOAttribute) eoproperty;
+ eoattribute = attributesByName.objectForKey(attributeName);
+ if (eoattribute == null) {
+ eoattribute = ERXQueryEOAttribute.create(mainEntity, attributeName, definition, destinationAttribute);
+ attributesByName.setObjectForKey(eoattribute, attributeName);
+ }
+ }
+ return eoattribute;
+ }
+
+ // Else destination property is an EORelationship
+ EORelationship eorelationship = (EORelationship) eoproperty;
+
+ // keyPath is a relationship key path
+ String relationshipKeyPath = keyPath;
+
+ // However, for the query we need to fetch the foreign key which we will later use
+ // to create the enterprise object fault from it.
+ EOAttribute sourceAttribute = eorelationship.sourceAttributes().lastObject();
+
+ // Compute the key path to the relationship's source attribute
+ String sourceAttributeKeyPath;
+
+ // if the key path has a single key, i.e. "customer" then simply use the existing
+ // source attribute from the relationship, i.e. "customerID". On the other hand
+ // if key path has multiple keys, i.e. "customer.shippingAddress" then compute
+ // the source attribute key path, i.e. "customer.shippingAddressID" and create
+ // an ad hoc attribute using the source attribute key path as the name of the
+ // attribute and the definition.
+ keys.removeLastObject();
+ EOAttribute eoattribute;
+ if (keys.count() == 0) {
+ eoattribute = sourceAttribute;
+ sourceAttributeKeyPath = sourceAttribute.name();
+ attributesByName.setObjectForKey(eoattribute, eoattribute.name());
+ } else {
+ keys.addObject(sourceAttribute.name());
+ sourceAttributeKeyPath = keys.componentsJoinedByString(".");
+ String attributeName = sourceAttributeKeyPath;
+ String definition = sourceAttributeKeyPath;
+
+ // Look to see if one has been created first
+ eoattribute = attributesByName.objectForKey(attributeName);
+ if (eoattribute == null) {
+ eoattribute = ERXQueryEOAttribute.create(mainEntity, attributeName, definition, sourceAttribute);
+ attributesByName.setObjectForKey(eoattribute, eoattribute.name());
+ }
+ }
+
+ EOEntity destinationEntity = eorelationship.destinationEntity();
+ RelationshipKeyInfo relationshipKeyInfo = new RelationshipKeyInfo(relationshipKeyPath, sourceAttributeKeyPath, destinationEntity);
+ relationshipKeysSet.addObject(relationshipKeyInfo);
+
+ // Return the ad hoc attribute that we created to fetch the foreign key
+ return eoattribute;
+ }
+
+ protected RuntimeException unknownPropertyException(String keyPath) {
+ return new RuntimeException("Unable to obtain property for key path '"
+ + keyPath + "' starting on the " + mainEntity.name() + " entity.");
+ }
+
+ /**
+ * Returns the destination entity for this report attribute. For example, if keyPath is
+ * provider.specialtyCategory then this method would return the SpecialtyCategory entity.
+ */
+ public static EOEntity destinationEntity(EOEntity rootEntity, String keyPath) {
+ EOEntity entity = rootEntity;
+ StringTokenizer t = new StringTokenizer(keyPath, ".");
+
+ while (t.hasMoreTokens()) {
+ String key = t.nextToken();
+ EORelationship relationship = entity.anyRelationshipNamed(key);
+ if (relationship != null) {
+ entity = relationship.destinationEntity();
+ }
+ }
+
+ return entity;
+ }
+
+ /**
+ * Returns whether the property (either EOAttribute or EORelationship) referenced by
+ * the last component in the key path.
+ */
+ public static EOProperty destinationProperty(EOEntity rootEntity, String keyPath) {
+ EOEntity entity = rootEntity;
+ String[] keys = keyPath.split("\\.");
+ EOAttribute attribute = null;
+ EORelationship relationship = null;
+
+ for (String key : keys) {
+ relationship = entity.anyRelationshipNamed(key);
+ if (relationship != null) {
+ entity = relationship.destinationEntity();
+ attribute = null;
+ } else {
+ attribute = entity.anyAttributeNamed(key);
+ }
+ }
+
+ if (attribute != null) {
+ return attribute;
+ } else if (relationship != null) {
+ return relationship;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a new EOSQLExpressionFactory for the entity and editing context specified.
+ */
+ protected static EOSQLExpressionFactory sqlExpressionFactory(EOEntity anEntity, EOEditingContext ec) {
+ EOModel model = anEntity.model();
+ EODatabaseContext databaseContext = EODatabaseContext.registeredDatabaseContextForModel(model, ec);
+ return databaseContext.adaptorContext().adaptor().expressionFactory();
+ }
+
+ /**
+ * Returns a qualifier by combining the restricting qualifiers (if any) in
+ * the entities referenced. The resulting qualifier is rooted at mainEntity.
+ * When this method is called the editingContext, mainEntity, mainSelectQualifier
+ * and selectAttributes i-vars must be set.
+ */
+ protected EOQualifier restrictingQualifierForReferencedEntities() {
+ // Get expression similar to what will be used to build the SQL
+ EOFetchSpecification spec = new EOFetchSpecification(mainEntity.name(), mainSelectQualifier, null);
+ EOSQLExpression e = sqlExpressionFactory(mainEntity, editingContext).selectStatementForAttributes(selectAttributes, false, spec, mainEntity);
+
+ // Array to hold the restricting qualifiers for each referenced entity
+ NSMutableArray qualifiers = new NSMutableArray();
+
+ // See what relationship paths are being traversed and check for
+ // destination entities having a restricting qualifier. The
+ // aliasesByRelationshipPath().allKeys() returns an array of
+ // strings like this:
+ // ("", "claimWorkflowReasons", "claimWorkflowReasons.workflowReason")
+ //
+ NSArray relationshipPaths = e.aliasesByRelationshipPath().allKeys();
+ for (String relationshipPath : relationshipPaths) {
+ EOEntity destinationEntity = null;
+ if (relationshipPath.length() == 0) continue;
+ destinationEntity = ERXQuery.destinationEntity(mainEntity, relationshipPath);
+ EOQualifier restrictingQualifier = destinationEntity.restrictingQualifier();
+ if (restrictingQualifier != null) {
+ ERXKey