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:

+ *
+ *  {@code
+ *  NSArray> records =
+ *      ERXQuery.create()
+ *          .select (keys)
+ *          .from (entity)
+ *          .where (qualifier)
+ *          .groupBy (groupings)
+ *          .having (havingQualifier)
+ *          .orderBy (sortings)
+ *          .fetch();
+ *  }
+ *  
+ * + *

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

+ *
+ *  {@code
+ *  NSDictionary recordInitializationValues = new NSDictionary<>((Object)2017, "preferredYear");
+ *  NSArray objs = query.fetch(editingContext, recordInitializationValues, Foo.class);
+ *  Foo aFoo = objs.lastObject();
+ *  int preferredYear = aFoo.preferredYear(); // i.e. 2017
+
+ *  }
+ *  
+ *

Defining Ad Hoc Attributes in the Entity

+ * + *

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

+ *
+ *  {@code
+ *  // Fetch last year's customer order totals exceeding $1000 in descending order
+ *  NSArray lastYearTopSales =
+ *      ERXQuery.create()
+ *          .select (Order.CUSTOMER)             // customer to-one
+ *          .select (Order.SUM_TOTAL_AMOUNT)     // non-class property defined as SUM(totalAmount)
+ *          .from (Order.ENTITY_NAME)
+ *          .where (lastYearQualifier)
+ *          .groupBy (Order.CUSTOMER)
+ *          .having (Order.SUM_TOTAL_AMOUNT.greaterThan(1000.00))
+ *          .orderBy (Order.SUM_TOTAL_AMOUNT.desc())
+ *          .fetch(editingContext, OrderSummary.class);
+ *  
+ *  // Peek at top sale record
+ *  OrderSummary topSale = ERXArrayUtilities.firstObject(lastYearTopSales);
+ *  if (topSale != null) {
+ *      System.out.println("Customer " + topSale.customer().fullName() 
+ *          + " ordered " + moneyFormatter.format(topSale.sumTotalAmount()));
+ *  }
+ *  }
+ *  
+ * + *

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

+ * + *

Defaults for Behavior Properties

+ *
    + *
  1. er.extensions.eof.ERXQuery.useBindVariables=false
  2. + *
  3. er.extensions.eof.ERXQuery.useEntityRestrictingQualifiers=true
  4. + *
  5. er.extensions.eof.ERXQuery.removesForeignKeysFromRowValues=true
  6. + *
+ * + * + * @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: + *

+ * + *

1. Define your custom record class

+ *
+	 * {@code
+	 * public class Foo {
+	 *    NSMutableDictionary data;
+	 *    
+	 *    // Constructor
+	 *    public Foo(EOEditingContext ec, NSMutableDictionary row) {
+	 *       data = row;
+	 *    }
+	 * }
+	 * }
+	 * 
+ * + *

2. Fetch the records

+ *
+	 * {@code
+	 * EOEditingContext editingContext = ERXEC.newEditingContext();
+	 * ERXQuery query = ...;
+	 * 
+	 * // This assumes Java <= 7
+	 * ERXQuery.RecordConstructor recordConstructor =
+	 *    new ERXQuery.RecordConstructor(){
+	 *    
+	 *       @Override
+	 *       public Foo constructRecord(EOEditingContext ec, NSMutableDictionary row) {
+	 *          return new Foo(ec, row);
+	 *       }
+	 *       
+	 *    };
+	 * 
+	 * NSArray foos = query.fetch(editingContext, recordConstructor);
+	 * 
+	 * // This assumes Java >= 8
+	 * NSArray foos = query.fetch(editingContext, (ec, row) -> new Foo(ec, row));
+	 * }
+	 * 
+ */ + 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 relationshipPathKey = new ERXKey (relationshipPath); + qualifiers.addObject(relationshipPathKey.prefix(restrictingQualifier)); + } + } + + if (qualifiers.count() == 1) { + return qualifiers.objectAtIndex(0); + } else if (qualifiers.count() > 1) { + return ERXQ.and(qualifiers); + } + + return null; + } + + /** + * Returns the SQL string corresponding to attribute a. The EOSQLExpression e + * must correspond to the expression being built to which must include attribute + * a as one of the select the attributes. + */ + protected String sqlStringForAttribute(EOSQLExpression e, EOAttribute a) { + String readFormat = a.readFormat(); + if (readFormat != null) { + return e.formatSQLString(e.sqlStringForAttribute(a), readFormat); + } + String attrSql = e.sqlStringForAttribute(a); + + // Strip out any column aliases hardcoded into the attribute name. + // Note that, to be stripped, the AS keyword, in caps, must be used. + int i = attrSql.indexOf(" AS "); + if (i > 0) { + return attrSql.substring(0, i); + } + + return attrSql; + } + + /** + * Returns the SQL string for the ordering of attribute a using the specified selector. + * The EOSQLExpression e must correspond to the expression being built which must include + * attribute a as one of the select attributes. + */ + public String sqlStringForOrderingAttribute(EOSQLExpression e, EOAttribute orderingAttribute, NSSelector selector) { + EOAttribute a = orderingAttribute; + String sqlString = sqlStringForAttribute(e, a); + + if (selector == EOSortOrdering.CompareCaseInsensitiveAscending) { + if (a.adaptorValueType() == 1) { + return "UPPER(" + sqlString + ") ASC"; + } + return sqlString + " ASC"; + + } else if (selector == EOSortOrdering.CompareCaseInsensitiveDescending) { + if (a.adaptorValueType() == 1) { + return "UPPER(" + sqlString + ") DESC"; + } + return sqlString + " DESC"; + + } else if (selector == EOSortOrdering.CompareAscending) { + return sqlString + " ASC"; + } else if (selector == EOSortOrdering.CompareDescending) { + return sqlString + " DESC"; + } else { + return "(" + sqlString + ")"; + } + } + + + /** + * Returns the SQL for the EOSQLExpression specified but with the place holder + * characters (?) replaced with their corresponding value from the bindings and + * formatted for in-line use. + */ + protected String sqlWithBindingsInline(String sql, EOSQLExpression expression) { + Pattern p = Pattern.compile("('[^']*')|(([,]?+)([\\\\?]{1}+))"); + Matcher m = p.matcher(sql); + StringBuffer inlineSql = new StringBuffer(); + + NSArray> bindVariableDictionaries = expression.bindVariableDictionaries(); + for (NSDictionary binding : bindVariableDictionaries) { + // Get the binding attribute, value and formatted value for inline use + EOAttribute attribute = (EOAttribute) binding.objectForKey(EOSQLExpression.BindVariableAttributeKey); + Object value = binding.objectForKey(EOSQLExpression.BindVariableValueKey); + String formattedValue = formatValueForAttributeForInlineUse(expression, value, attribute); + + // Search until placeholder is replaced + + while (m.find()) { + String singleQuoteLiteral = m.group(1); + if (singleQuoteLiteral != null) { + // Append single quote literal which may include a ? character and + // continue to look for a legitimate ? placeholder character for + // the binding's value + m.appendReplacement(inlineSql, Matcher.quoteReplacement(singleQuoteLiteral)); + } else { + // A legitimate ? placeholder character was found which may be optionally + // preceded with a comma. Put the comma back in there if any ($3) followed + // by the formatted binding value. + String replacement = "$3" + Matcher.quoteReplacement(formattedValue); + m.appendReplacement(inlineSql, replacement); + break; // <--- EXIT: We're done searching/replacing the placeholder for this binding + } + } + } + m.appendTail(inlineSql); + + return inlineSql.toString(); + } + + /** + * Returns the SQL for the EOSQLExpression specified but with the place holder + * characters (?) replaced with their corresponding value from the bindings and + * formatted for inline use. + */ + protected String sqlWithBindingsInline2(String sql, EOSQLExpression expression) { + StringBuilder newSql = new StringBuilder(sql.length() + 100); + + NSArray> bindVariableDictionaries = expression.bindVariableDictionaries(); + + char chars[] = sql.toCharArray(); + int offset = 0; + + for (NSDictionary binding : bindVariableDictionaries) { + + // Get the binding value and attribute + Object value = binding.objectForKey(EOSQLExpression.BindVariableValueKey); + EOAttribute attribute = (EOAttribute) binding.objectForKey(EOSQLExpression.BindVariableAttributeKey); + + // Format the value for the binding + + String formattedValue = formatValueForAttributeForInlineUse(expression, value, attribute); + + // Append sql up to the to bind variable place holder + while (offset < chars.length && chars[offset] != '?') { + newSql.append(chars[offset]); + offset++; + } + + // Now append the formatted value instead of the bind variable place holder + + newSql.append(formattedValue); + offset++; + } + + // Now append the remaining sql + + while (offset < chars.length) { + newSql.append(chars[offset]); + offset++; + } + + return newSql.toString(); + } + + /** + * Uses the EOSQLExpression provided to get the SQL string for value and + * corresponding attribute. This method is similar to EOSQLExpression's + * sqlStringForValue(Object value, String keyPath) but this one does not + * attempt to get to the attribute from the key path as we already have + * the attribute. + */ + 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); + } + + /** + * Formats the value for inline use. For example, the string "I'm smart" would be formatted + * as "'I''m smart'". Similarly a NSTimestamp value would be converted to something like + * this "TO_DATE('1967-12-03 00:15:00','YYYY-MM-DD HH24:MI:SS')". Supported values are null, + * String, NSTimestamp, Boolean, Integer, Number. Other values are converted + * by calling toString(). + */ + protected String formatValueForAttributeForInlineUse(EOSQLExpression sqlExpression, Object value, EOAttribute attribute) { + String formattedValue; + + if (value == null || value == NSKeyValueCoding.NullValue) { + formattedValue = "NULL"; + } else { + // First try to see if the EOSQLExpression's formatValueForAttribute() + // knows how to format the value for the corresponding attribute + formattedValue = sqlExpression.formatValueForAttribute(value, attribute); + + // If the formattedValue is "NULL" then the formatValueForAttribute() did not + // do its job and we'll do the best we can here + if (formattedValue == null || formattedValue.equals("NULL")) { + if (value instanceof String) { + formattedValue = sqlExpression.formatStringValue((String)value); + } else if (value instanceof NSTimestamp) { + NSTimestamp timestamp = (NSTimestamp) value; + formattedValue = formattedTimestampForInlineUse(sqlExpression, timestamp, attribute); + } else if (value instanceof Boolean) { + boolean boolValue = (Boolean) value; + // If stored in the database as a string then format as string + // otherwise format as a number 1 or 0. + if (attribute.externalType().toLowerCase().contains("char")) { + formattedValue = "'" + boolValue +"'"; + } else if (boolValue) { + formattedValue = EOSQLExpression.sqlStringForNumber(1); + } else { + formattedValue = EOSQLExpression.sqlStringForNumber(0); + } + } else { + formattedValue = value.toString(); + } + } + } + + if (log.isDebugEnabled()) { + log.debug(this.getClass().getSimpleName() + " formatted value " + value + " for inline use as " + formattedValue); + } + + return formattedValue; + } + + protected String databaseProductName(EOEntity entity) { + JDBCAdaptor adaptor = (JDBCAdaptor) EOAdaptor.adaptorWithModel(entity.model()); + JDBCPlugIn plugin = adaptor.plugIn(); + return plugin.databaseProductName().toLowerCase(); + } + + protected String formattedTimestampForInlineUse(EOSQLExpression sqlExpression, NSTimestamp timestamp, EOAttribute attribute) { + EOEntity entity = attribute.entity(); + String databaseProductName = databaseProductName(entity); + //NSTimestampFormatter formatter = new NSTimestampFormatter("%Y-%m-%d %H:%M:%S"); + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String formattedValue = formatter.format(timestamp); + + // Only oracle has been tested - the rest were guessed by searching + // the web on how to convert string to date / timestamp in <> + + // Now wrap the formatted value with the string-to-date function + // corresponding to the database product being used + switch (databaseProductName) { + case "oracle": + // See http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions183.htm + formattedValue = "TO_DATE('" + formattedValue + "', 'YYYY-MM-DD HH24:MI:SS')"; + case "postgresql": + // See https://www.postgresql.org/docs/7.4/static/functions-formatting.html + formattedValue = "TO_DATE('" + formattedValue + "', 'YYYY-MM-DD HH24:MI:SS')"; + break; + case "mysql": + // See https://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_str-to-date + formattedValue = "STR_TO_DATE('" + formattedValue + "', '%Y-%m-%d %H:%i:%s')"; + break; + case "h2": + // See from http://www.h2database.com/html/functions.html + formattedValue = "PARSEDATETIME('" + formattedValue + "', '" + formatter.toPattern() + "')"; + break; + case "derby": + // I got this from http://community.teradata.com/t5/UDA/convert-varchar-to-timestamp/td-p/32302 + formattedValue = "CAST('" + formattedValue + "' AS TIMESTAMP(0) FORMAT 'YYYY-MM-DDBHH:MI:SS')"; + break; + case "openbase": + case "frontbase": + // This is a wild guess.... I did not find anything on the web for these two database products + formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); + formattedValue = "'" + formatter.format(timestamp) + "'"; + break; + case "microsoft": + // See https://msdn.microsoft.com/en-us/library/ms180878(SQL.100).aspx#ISO8601Format + // See https://msdn.microsoft.com/en-us/library/ms187928(v=sql.90).aspx + // See http://stackoverflow.com/questions/207190/sql-server-string-to-date-conversion + formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); + formattedValue = "CAST('" + formattedValue + "' AS datetime2)"; + break; + default: + throw new RuntimeException( + "Please add support to ERXQuery's for formatting NSTimestamp values for database product " + + databaseProductName + ); + } + + + return formattedValue; + } + + + + + + // + // The RecordConstructor functional interface + // + + + /** + * Functional interface for constructing a record from a dictionary + * with the data fetched from the database. + */ + public static interface RecordConstructor { + public abstract T constructRecord(EOEditingContext ec, NSMutableDictionary row); + } + + /** + * This is the default constructor used by the fetch() and fetch(EOEditingContext) + * methods. It simply returns the row dictionary passed in. + */ + public static class DefaultRecordConstructor implements RecordConstructor> { + @Override + public NSDictionary constructRecord(EOEditingContext ec, NSMutableDictionary row) { + return row; + } + } + + + // + // The EntityModificationAction class + // + + public static abstract class EntityModificationAction { + + protected abstract void modifyEntity(EOEntity entity); + + public void run(EOEditingContext ec, EOEntity entity) { + ec.lock(); + try { + String modelName = entity.model().name(); + EODatabaseContext dbc = EOUtilities.databaseContextForModelNamed(ec, modelName); + dbc.lock(); + try { + modifyEntity(entity); + } finally { + dbc.unlock(); + } + } finally { + ec.unlock(); + } + } + } + + + // Statistics + + public double queryEvaluationTime() { + return queryEvaluationTime; + } + +} diff --git a/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQueryAttributes.java b/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQueryAttributes.java new file mode 100644 index 00000000000..8c37a880b7d --- /dev/null +++ b/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQueryAttributes.java @@ -0,0 +1,44 @@ +package er.extensions.eof; + +import com.webobjects.eoaccess.EOAttribute; +import com.webobjects.eoaccess.EOEntity; +import com.webobjects.foundation.NSMutableArray; + +/** + *

ERXQueryAttributes.java

+ * + *

+ * A convenient subclass of NSMutableArray to make it easy to create multiple + * ad hoc attributes that can be used with ERXQuery. It uses a fluent style API so that + * it can be used like this: + *

+ * {@code
+ * NSArray attributes =
+ *     ERXQueryAttributes.create(claimEntity)
+ *         .add("providerFullName", "provider.fullName", "varchar50")
+ *         .add("claimCount", "COUNT(DISTINCT claimID)", "intNumber");
+ * }
+ * 
+ *

+ * @author Ricardo J. Parada + */ + +@SuppressWarnings("javadoc") + +public class ERXQueryAttributes extends NSMutableArray { + protected EOEntity _entity; + + protected ERXQueryAttributes(EOEntity entity) { + this._entity = entity; + } + + public static ERXQueryAttributes create(EOEntity entity) { + return new ERXQueryAttributes(entity); + } + + public ERXQueryAttributes add(String name, String definition, String prototype) { + EOAttribute attr = ERXQueryEOAttribute.create(_entity, name, definition, prototype); + addObject(attr); + return this; + } +} \ No newline at end of file diff --git a/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQueryEOAttribute.java b/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQueryEOAttribute.java new file mode 100644 index 00000000000..fd5a527467f --- /dev/null +++ b/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/ERXQueryEOAttribute.java @@ -0,0 +1,124 @@ +package er.extensions.eof; + +import com.webobjects.eoaccess.EOAttribute; +import com.webobjects.eoaccess.EOEntity; + +/** + *

ERXQueryEOAttribute.java

+ *

+ * This subclass of EOAttribute allows you to define attributes with a definition + * such as "COUNT(DISTINCT lineItems.lineItemID)" that can later be used with + * ERXQuery. + *

+ *

+ * What makes this class different from EOAttribute is that you do not have to + * add the attribute to the entity but you do have to tell the attribute what + * entity to use via the setAdHocEntity() method. The entity is used when + * looking up the properties referenced in the definition of the attribute, + * i.e. "SUM(lineItems.extendedAmount)". + *

+ *

+ * Instances of this class can be used with ERXQuery's select() and groupBy() + * methods. Their names can be referenced by the orderings passed into + * the orderBy() method and by qualifiers passed into the where() and having() + * methods. + *

+ *

+ * This class provides a factory method to create an instance of this class + * which encapsulates the correct order in which the name, entity, prototype + * and definition must be set for the attribute to work properly during + * SQL generation. + *

+ * + * @author Ricardo J. Parada + */ + +@SuppressWarnings("javadoc") + +public class ERXQueryEOAttribute extends EOAttribute { + + protected void setAdHocEntity(EOEntity entity) { + _parent = entity; + } + + /** + * Override to make sure that simple definitions like "SYSDATE" work okay. + */ + @Override + public void setDefinition(String definition) { + // If the definition becomes null after setting it then let's try using the + // readFormat instead. For example, super.setDefinition("SYSDATE") does not work. On + // the other hand calling setReadFormat("SYSDATE") and setColumnName("") does the + // trick. In general super.setDefinition(definition) does not seem to work for + // simple oracle function that don't even have parenthesis like "SYSDATE". + super.setDefinition(definition); + + // If the definition was not set above then wrap it in parenthesis + if (definition() == null) { + setReadFormat(definition); + setColumnName(""); + } + } + + // Factory method + + /** + * Creates an instance of this class with the given name, definition and prototype + * attribute name. For example: + *
+	 * {@code
+	 * EOAttribute attr = 
+	 *     ERXQueryEOAttribute.create(
+	 *         claimEntity, "totalExpectedAmount", "SUM(expectedAmount)", "currencyAmount2"
+	 *     );
+	 * }
+	 * 
+ */ + public static EOAttribute create(EOEntity entity, String name, String definition, String prototypeAttributeName) { + // Get prototype attribute + EOAttribute prototype = entity.model().prototypeAttributeNamed(prototypeAttributeName); + + // Create attribute, set name and ad hoc entity *immediately before* + // setting the prototype and the definition + ERXQueryEOAttribute attr = new ERXQueryEOAttribute(); + attr.setName(name); + attr.setAdHocEntity(entity); + + // The prototype must be set *after* the ad hoc entity + attr.setPrototype(prototype); + + // The definition must be set *after* the prototype + attr.setDefinition(definition); + + return attr; + } + + public static ERXQueryEOAttribute create(EOEntity entity, String name, String definition, EOAttribute similarAttribute) { + // Create attribute, set name and ad hoc entity *immediately before* + // setting the prototype and the definition + ERXQueryEOAttribute attr = new ERXQueryEOAttribute(); + attr.setName(name); + attr.setAdHocEntity(entity); + + // The prototype must be set *after* the ad hoc entity + EOAttribute prototype = similarAttribute.prototype(); + if (prototype != null) { + attr.setPrototype(prototype); + } else { + attr.setClassName(similarAttribute.className()); + attr.setExternalType(similarAttribute.externalType()); + attr.setValueFactoryMethodName(similarAttribute.valueFactoryMethodName()); + attr.setFactoryMethodArgumentType(similarAttribute.factoryMethodArgumentType()); + attr.setAdaptorValueConversionClassName(similarAttribute.adaptorValueConversionClassName()); + attr.setAdaptorValueConversionMethodName(similarAttribute.adaptorValueConversionMethodName()); + attr.setValueType(similarAttribute.valueType()); + attr.setWidth(similarAttribute.width()); + attr.setPrecision(similarAttribute.precision()); + } + // The definition must be set *after* the prototype + attr.setDefinition(definition); + + return attr; + } + +} \ No newline at end of file diff --git a/Frameworks/Misc/ERPDFGeneration/Libraries/flying-saucer-pdf-9.0.7.jar b/Frameworks/Misc/ERPDFGeneration/Libraries/flying-saucer-pdf-9.0.7.jar new file mode 100644 index 00000000000..2aae046e331 Binary files /dev/null and b/Frameworks/Misc/ERPDFGeneration/Libraries/flying-saucer-pdf-9.0.7.jar differ diff --git a/Frameworks/Misc/ERPDFGeneration/Libraries/flying-saucer-swt-9.0.7.jar b/Frameworks/Misc/ERPDFGeneration/Libraries/flying-saucer-swt-9.0.7.jar new file mode 100644 index 00000000000..416fb821522 Binary files /dev/null and b/Frameworks/Misc/ERPDFGeneration/Libraries/flying-saucer-swt-9.0.7.jar differ