//
// ERXInQualifier.java
// Project EOInQualifier
//
// Created by max on Mon Jul 15 2002
//
package er.extensions.eof.qualifiers;
import java.util.Enumeration;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EOJoin;
import com.webobjects.eoaccess.EOQualifierSQLGeneration;
import com.webobjects.eoaccess.EORelationship;
import com.webobjects.eoaccess.EOSQLExpression;
import com.webobjects.eoaccess.EOUtilities;
import com.webobjects.eocontrol.EOAndQualifier;
import com.webobjects.eocontrol.EOClassDescription;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.eocontrol.EOKeyValueArchiver;
import com.webobjects.eocontrol.EOKeyValueQualifier;
import com.webobjects.eocontrol.EOKeyValueUnarchiver;
import com.webobjects.eocontrol.EOObjectStoreCoordinator;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOQualifierVariable;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSCoder;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSKeyValueCodingAdditions;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSSet;
import er.extensions.eof.ERXEOAccessUtilities;
import er.extensions.foundation.ERXProperties;
import er.extensions.jdbc.ERXSQLHelper;
import er.extensions.qualifiers.ERXKeyValueQualifier;
/**
* The ERXInQualifier is useful for creating qualifiers that
* will generate SQL using the 'IN' key word.<br>
* <br>
* For example constructing this qualifer:<br>
* <code>ERXInQualifier q = new ERXInQualifier("userId", arrayOfNumbers);</code>
* Then this qualifier would generate SQL of the form:
* USER_ID IN (<array of numbers or data>)
*/
// ENHANCEME: Should support restrictive qualifiers, don't need to subclass KeyValueQualifier
public class ERXInQualifier extends ERXKeyValueQualifier implements Cloneable {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
private static final int DefaultPadToSize =
ERXProperties.intForKeyWithDefault("er.extensions.ERXInQualifier.DefaultPadToSize", 8);
/** register SQL generation support for the qualifier */
static {
EOQualifierSQLGeneration.Support.setSupportForClass(new InQualifierSQLGenerationSupport(), ERXInQualifier.class);
}
/**
* Constructs an in qualifer for a given
* attribute name and an array of values.
* @param key attribute name
* @param values array of values
*/
public ERXInQualifier(String key, NSArray values) {
this(key, values, DefaultPadToSize);
}
/**
* Constructs an in qualifier for a given
* attribute name and an array of values.
* @param key attribute name
* @param values array of values
* @param padToSize the size which is expected to be reasonable for this qualifier. If the NSArray values
* has more than one element, the padToSize is used to round up the number of elements and pad with nulls.
* Doing this reduces the number of unique queries which result from having an arbitrary number of values.
* For example, if the padToSize is 8, then we'll either have 1, or 8, or 16, or... bind variables as
* compared to 1..., 2..., 3..., 4..., or ....16
*/
public ERXInQualifier(String key, NSArray values, final int padToSize) {
super(key, EOQualifier.QualifierOperatorEqual, paddedValues(values, padToSize));
}
/**
* @param values see ERXInQualifier
* @param padToSize see ERXInQualifier
* @return an NSArray with a count that is an even multiple of padToSize and padded with the last element of the
* values array.
*/
private static NSArray paddedValues(NSArray values, final int padToSize) {
final int count = values.count();
if (count > 1) {
final int paddedSize = (((count - 1) / padToSize) + 1) * padToSize;
final NSMutableArray paddedValues = new NSMutableArray(values);
int padCount = paddedSize - count;
// We pad with the last element repeated padCount times. Do not pad with null
// as that might extend the set inadvertently.
Object padElement = values.lastObject();
while (padCount > 0) {
paddedValues.addObject(padElement);
padCount--;
}
values = paddedValues;
}
return values;
}
/**
* String representation of the in
* qualifier.
* @return string description of the qualifier
*/
@Override
public String toString() {
return " <" + getClass().getName() + " key: " + key() + " > IN '" + value() + "'";
}
public NSArray values() {
return (NSArray)value();
}
/** Tests if the given object's key is in the supplied values */
// FIXME: this doesn't work right with EOs when the key() is keypath across a relationship
@Override
public boolean evaluateWithObject(Object object) {
Object value = null;
String key = key();
if(object instanceof EOEnterpriseObject) {
EOEnterpriseObject eo = (EOEnterpriseObject)object;
EOEditingContext ec = eo.editingContext();
EOClassDescription cd = eo.classDescription();
if (cd.attributeKeys().containsObject(key)) {
value = NSKeyValueCodingAdditions.Utility.valueForKeyPath(eo, key);
} else if (cd.toOneRelationshipKeys().containsObject(key)) {
value = eo.valueForKeyPath(key);
} else if (EOUtilities.entityNamed(ec, eo.entityName()).primaryKeyAttributeNames().containsObject(key)) {
// when object is an EO and key is a cross-relationship keypath, we drop through to this case
// and we'll fail.
value = EOUtilities.primaryKeyForObject(ec,eo).objectForKey(key);
} else {
// ok, it could be any key-path not directly listed as a to-one relationship
value = NSKeyValueCodingAdditions.Utility.valueForKeyPath(eo, key);
if (value instanceof NSArray) {
// here we will only handle to-many (NSArray).
// so we try to compare 'n' values with 'm' objects.
// we use set intersection
NSSet vs = new NSSet((NSArray) value);
NSSet vss = new NSSet(values());
return vs.intersectsSet(vss);
}
}
} else {
value = NSKeyValueCodingAdditions.Utility.valueForKeyPath(object, key);
}
return value != null && values().containsObject(value);
}
/**
* EOF seems to be wanting to clone qualifiers when
* they are inside an and-or qualifier without this
* method, ERXInQualifier is cloned into
* an EOKeyValueQualifier and the generated SQL is incorrect...
* @return cloned primary key list qualifier.
*/
@Override
public Object clone() {
return new ERXInQualifier(key(), values());
}
/**
* Adds SQL generation support. Note that the database needs to support
* the IN operator.
*/
public static class InQualifierSQLGenerationSupport extends EOQualifierSQLGeneration._KeyValueQualifierSupport {
/**
* Public constructor
*/
public InQualifierSQLGenerationSupport() {
super();
}
/**
* Generates the SQL string for an ERXInQualifier.
* @param eoqualifier an in qualifier
* @param e current eo sql expression
* @return SQL for the current qualifier.
*/
@Override
public String sqlStringForSQLExpression(EOQualifier eoqualifier, EOSQLExpression e) {
ERXInQualifier inqualifier = (ERXInQualifier)eoqualifier;
String result;
if (inqualifier.value() instanceof NSArray) {
String key = inqualifier.key();
result = ERXSQLHelper.newSQLHelper(e).sqlWhereClauseStringForKey(e, key, (NSArray) inqualifier.value());
} else {
throw new RuntimeException("Unsupported value type: " + inqualifier.value().getClass().getName());
}
return result;
}
// ENHANCEME: This should support restrictive qualifiers on the root entity
@Override
public EOQualifier schemaBasedQualifierWithRootEntity(EOQualifier eoqualifier, EOEntity eoentity) {
EOKeyValueQualifier eokeyvaluequalifier = (EOKeyValueQualifier)eoqualifier;
String key = eokeyvaluequalifier.key();
EORelationship eorelationship = eoentity._relationshipForPath(key);
if(eorelationship == null) {
if(!(eokeyvaluequalifier instanceof ERXInQualifier)) {
eokeyvaluequalifier = new ERXInQualifier(key, (NSArray) eokeyvaluequalifier.value());
}
return eokeyvaluequalifier;
}
if(eorelationship.isFlattened()) {
eorelationship = ERXEOAccessUtilities.lastRelationship(eorelationship);
}
NSArray joins = eorelationship.joins();
int l = joins.count();
NSMutableArray destinationAttibuteNames = new NSMutableArray(l);
for(int i = l - 1; i >= 0; i--) {
destinationAttibuteNames.addObject(((EOJoin)joins.objectAtIndex(i)).destinationAttribute().name());
}
Object value = eokeyvaluequalifier.value();
Object obj;
if(value == NSKeyValueCoding.NullValue || (value instanceof EOQualifierVariable)) {
NSMutableDictionary mapping = new NSMutableDictionary(l);
for(int j = 0; j < l; j++) {
mapping.setObjectForKey(value, destinationAttibuteNames.objectAtIndex(j));
}
obj = mapping;
} else {
NSMutableDictionary mapping = new NSMutableDictionary(l);
for(int j = 0; j < l; j++) {
NSMutableArray realValues = new NSMutableArray();
for(Enumeration e = ((NSArray)value).objectEnumerator(); e.hasMoreElements();) {
Object o = e.nextElement();
NSDictionary dict = null;
String currentKey = (String) destinationAttibuteNames.objectAtIndex(j);
Object v;
if (o instanceof EOEnterpriseObject) {
EOEnterpriseObject eoenterpriseobject = (EOEnterpriseObject)o;
EOObjectStoreCoordinator osc = ((EOObjectStoreCoordinator)eoenterpriseobject.editingContext().rootObjectStore());
dict = osc.valuesForKeys(new NSArray(currentKey), eoenterpriseobject);
v = dict.objectForKey(currentKey);
} else {
v = o;
}
realValues.addObject(v);
}
mapping.setObjectForKey(realValues, destinationAttibuteNames.objectAtIndex(j));
}
obj = mapping;
}
NSMutableArray qualifiers = null;
ERXInQualifier result = null;
l = destinationAttibuteNames.count();
for(int k = 0; k < l; k++) {
String s1 = (String)destinationAttibuteNames.objectAtIndex(k);
String s2 = _optimizeQualifierKeyPath(eoentity, key, s1);
Object o = ((NSDictionary) (obj)).objectForKey(s1);
result = new ERXInQualifier(s2, (NSArray) o);
if(l <= 1)
continue;
if(qualifiers == null) {
qualifiers = new NSMutableArray();
qualifiers.addObject(result);
}
}
if(qualifiers == null) {
return result;
}
return new EOAndQualifier(qualifiers);
}
@Override
public EOQualifier qualifierMigratedFromEntityRelationshipPath(EOQualifier eoqualifier, EOEntity eoentity, String s) {
// the key migration is the same as for EOKeyValueQualifier
ERXInQualifier inQualifier=(ERXInQualifier)eoqualifier;
String newPath = EOQualifierSQLGeneration.Support._translateKeyAcrossRelationshipPath(inQualifier.key(), s, eoentity);
return new ERXInQualifier(newPath, inQualifier.values());
}
}
@Override
public Class classForCoder() {
return getClass();
}
public static Object decodeObject(NSCoder coder) {
String key = (String) coder.decodeObject();
NSArray values = (NSArray)coder.decodeObject();
return new ERXInQualifier(key, values);
}
@Override
public void encodeWithCoder(NSCoder coder) {
coder.encodeObject(key());
coder.encodeObject(values());
}
@Override
public void encodeWithKeyValueArchiver(EOKeyValueArchiver archiver) {
archiver.encodeObject(key(), "key");
archiver.encodeObject(values(), "values");
}
public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver unarchiver) {
return new ERXInQualifier(
(String)unarchiver.decodeObjectForKey("key"),
(NSArray)unarchiver.decodeObjectForKey("values"));
}
}