package com.webobjects.jdbcadaptor;
import java.sql.Timestamp;
import com.webobjects.eoaccess.EOAttribute;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EOModel;
import com.webobjects.eoaccess.EORelationship;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSProperties;
import com.webobjects.foundation.NSTimestamp;
import com.webobjects.foundation.NSTimestampFormatter;
import com.webobjects.foundation._NSStringUtilities;
import com.webobjects.jdbcadaptor.OraclePlugIn.OracleExpression;
/** overrides OracleExpression in order to add
* TIMESTAMP values including milliseconds. The
* normal EOF Oracle PlugIn does not add milliseconds
* to the TIMESTAMP value
*
* @author David Teran
*
*/
public class EROracleExpression extends OracleExpression {
/**
* Holds array of join clauses.
*/
private final NSMutableArray _alreadyJoined = new NSMutableArray();
/**
* Fetch spec limit ivar
*/
private int _fetchLimit;
public static interface Delegate {
/**
* Returns the constraint name for the given relationship.
*
* @param relationship the relationship
* @param sourceColumns the source columns
* @param destinationColumns the destination columns
* @return the constraint name (or null for default)
*/
public String constraintStatementForRelationship(EORelationship relationship, NSArray sourceColumns, NSArray destinationColumns);
}
private static final NSTimestampFormatter _TIMESTAMP_FORMATTER = new NSTimestampFormatter("%Y-%m-%d %H:%M:%S.%F");
private static EROracleExpression.Delegate _delegate;
/**
* Sets the delegate for this expression.
*
* @param delegate the delegate for this expression
*/
public static void setDelegate(EROracleExpression.Delegate delegate) {
EROracleExpression._delegate = delegate;
}
public EROracleExpression(EOEntity eoentity) {
super(eoentity);
}
protected boolean shouldAllowNull(EOAttribute attribute) {
boolean shouldAllowNull = attribute.allowsNull();
// If you allow nulls, then there's never a problem ...
if (!shouldAllowNull) {
EOEntity entity = attribute.entity();
EOEntity parentEntity = entity.parentEntity();
String externalName = entity.externalName();
if (externalName != null) {
// If you have a parent entity and that parent entity shares your table name, then you're single table inheritance
boolean singleTableInheritance = (parentEntity != null && externalName.equals(parentEntity.externalName()));
if (singleTableInheritance) {
EOAttribute parentAttribute = parentEntity.attributeNamed(attribute.name());
if (parentAttribute == null) {
// If this attribute is new in the subclass, you have to allow nulls
shouldAllowNull = true;
}
}
}
}
return shouldAllowNull;
}
@Override
public void addCreateClauseForAttribute(EOAttribute attribute) {
NSDictionary userInfo = attribute.userInfo();
Object defaultValue = null;
if (userInfo != null) {
defaultValue = userInfo.valueForKey("er.extensions.eoattribute.default"); // deprecated key
if (defaultValue == null) {
defaultValue = userInfo.valueForKey("default");
}
}
String sql;
String allowsNullClauseForConstraint = allowsNullClauseForConstraint(shouldAllowNull(attribute));
if (defaultValue == null) {
sql = _NSStringUtilities.concat(attribute.columnName(), " ", columnTypeStringForAttribute(attribute), " ", allowsNullClauseForConstraint);
}
else {
sql = _NSStringUtilities.concat(attribute.columnName(), " ", columnTypeStringForAttribute(attribute), " DEFAULT ", formatValueForAttribute(defaultValue, attribute), " ", allowsNullClauseForConstraint);
}
appendItemToListString(sql, _listString());
}
/** Overridden in order to add milliseconds to the value. This
* applies only if obj is an instance of NSTimestamp and if
* valueType from the eoattribute is T
*
* @param obj
* @param eoattribute
*
* @return the modified bindVariableDictionary
*/
@Override
public NSMutableDictionary bindVariableDictionaryForAttribute(EOAttribute eoattribute, Object obj) {
NSMutableDictionary result = super.bindVariableDictionaryForAttribute(eoattribute, obj);
if((obj instanceof NSTimestamp) && (isTimestampAttribute(eoattribute))) {
NSTimestamp nstimestamp = (NSTimestamp)obj;
long millis = nstimestamp.getTime();
// AK: since NSTimestamp places fractional millis in the getTime,
// the driver is getting very confused and refuses to update the columns as
// they get translated to 0 as the fractional values.
Timestamp timestamp = new Timestamp(millis);
timestamp.setNanos(timestamp.getNanos()+nstimestamp.getNanos());
result.setObjectForKey(timestamp, "BindVariableValue");
}
return result;
}
/** Overridden in order to add milliseconds to the value. This
* applies only if obj is an instance of NSTimestamp and if
* valueType from the eoattribute is T
*
* @param obj
* @param eoattribute
*
* @return the modified string
*/
@Override
public String formatValueForAttribute(Object obj, EOAttribute eoattribute) {
String value;
if((obj instanceof NSTimestamp) && isTimestampAttribute(eoattribute)) {
value = "'" + _TIMESTAMP_FORMATTER.format(obj) + "'";
} else {
value = super.formatValueForAttribute(obj, eoattribute);
}
return value;
}
private boolean isTimestampAttribute(EOAttribute eoattribute) {
return "T".equals(eoattribute.valueType());
}
/**
* @return true to indicate that the Oracle jdbc driver should use
* bind variables
*/
@Override
public boolean useBindVariables() {
return true;
}
/**
* @return true to indicate that the Oracle jdbc driver should use
* bind variables
*/
@Override
public boolean shouldUseBindVariableForAttribute(EOAttribute attribute) {
return true;
}
/**
* @return true to indicate that the Oracle jdbc driver should use
* bind variables
*/
@Override
public boolean mustUseBindVariableForAttribute(EOAttribute attribute) {
return true;
}
@Override
public void prepareConstraintStatementForRelationship(EORelationship relationship, NSArray sourceColumns, NSArray destinationColumns) {
EOEntity entity = relationship.entity();
String tableName = entity.externalName();
int lastDot = tableName.lastIndexOf('.');
if (lastDot >= 0) {
tableName = tableName.substring(lastDot + 1);
}
String constraintName = null;
if (EROracleExpression._delegate != null) {
constraintName = EROracleExpression._delegate.constraintStatementForRelationship(relationship, sourceColumns, destinationColumns);
}
if (constraintName == null && entity != null) {
constraintName = System.getProperty("er.extensions.ERXModelGroup." + entity.name() + "." + relationship.name() + ".foreignKey");
}
if (constraintName == null) {
constraintName = _NSStringUtilities.concat(tableName, "_", relationship.name(), "_FK");
}
String sourceKeyList = sourceColumns.componentsJoinedByString(", ");
String destinationKeyList = destinationColumns.componentsJoinedByString(", ");
EOModel sourceModel = entity.model();
EOModel destModel = relationship.destinationEntity().model();
if (sourceModel != destModel && !sourceModel.connectionDictionary().equals(destModel.connectionDictionary())) {
throw new IllegalArgumentException("prepareConstraintStatementForRelationship unable to create a constraint for " + relationship.name() + " because the source and destination entities reside in different databases");
}
else {
setStatement("ALTER TABLE " + entity.externalName() + " ADD CONSTRAINT " + constraintName + " FOREIGN KEY (" + sourceKeyList + ") REFERENCES " + relationship.destinationEntity().externalName() + " (" + destinationKeyList + ") DEFERRABLE INITIALLY DEFERRED");
}
}
/**
* Overriden to handle correct placements of join conditions and to handle
* DISTINCT fetches with compareCaseInsensitiveA(De)scending sort orders.
* Lifted directly from the PostgressExpression.java class.
*
* @param attributes
* the attributes to select
* @param lock
* flag for locking rows in the database
* @param qualifier
* the qualifier to restrict the selection
* @param fetchOrder
* specifies the fetch order
* @param columnList
* the SQL columns to be fetched
* @param tableList
* the the SQL tables to be fetched
* @param whereClause
* the SQL where clause
* @param joinClause
* the SQL join clause
* @param orderByClause
* the SQL sort order clause
* @param lockClause
* the SQL lock clause
* @return the select statement
*/
@Override
public String assembleSelectStatementWithAttributes(NSArray attributes,
boolean lock,
EOQualifier qualifier,
NSArray fetchOrder,
String selectString,
String columnList,
String tableList,
String whereClause,
String joinClause,
String orderByClause,
String lockClause) {
StringBuilder sb = new StringBuilder();
sb.append(selectString);
sb.append(columnList);
// AK: using DISTINCT with ORDER BY UPPER(foo) is an error if it is not
// also present in the columns list...
// This implementation sucks, but should be good enough for the normal
// case
if (selectString.indexOf(" DISTINCT") != -1) {
String[] columns = orderByClause.split(",");
for (int i = 0; i < columns.length; i++) {
String column = columns[i].replaceFirst("\\s+(ASC|DESC)\\s*",
"");
column = column.replaceFirst("(NULLS\\sFIRST|NULLS\\sLAST)","");
if (columnList.indexOf(column) == -1) {
sb.append(", ");
sb.append(column);
}
}
}
sb.append(" FROM ");
String fieldString;
if (_alreadyJoined.count() > 0) {
fieldString = joinClauseString();
} else {
fieldString = tableList;
}
sb.append(fieldString);
if ((whereClause != null && whereClause.length() > 0)
|| (joinClause != null && joinClause.length() > 0)) {
sb.append(" WHERE ");
if (joinClause != null && joinClause.length() > 0) {
sb.append(joinClause);
if (whereClause != null && whereClause.length() > 0)
sb.append(" AND ");
}
if (whereClause != null && whereClause.length() > 0) {
sb.append(whereClause);
}
}
if (orderByClause != null && orderByClause.length() > 0) {
sb.append(" ORDER BY ");
sb.append(orderByClause);
}
if (lockClause != null && lockClause.length() > 0) {
sb.append(' ');
sb.append(lockClause);
}
if (_fetchLimit != 0) {
sb.append(" LIMIT ");
sb.append(_fetchLimit);
}
return sb.toString();
}
/**
* Overridden to allow the Null Sorting behavior of Oracle to be modified by
* setting an application property. There are three options:
*
* 1) Nulls always first, irrespective of sorting:
* EROraclePlugIn.nullSortBehavior=NullsFirst
*
* 2) Nulls always last, irrespective of sorting (this is Oracle's default):
* EROraclePlugIn.nullSortBehavior=NullsLast
*
* 3) Nulls as the least or smallest value, the same as EOF:
* EROraclePlugIn.nullSortBehavior=EOFStyle.
*
* If you want to use either NullsFirst or NullsLast, you will need to
* create a new EOSortOrdering.ComparisonSupport class and set it to be used
* at application startup otherwise EOF will still go and resort using nulls
* as the smallest value.
*
* @see com.webobjects.eoaccess.EOSQLExpression#addOrderByAttributeOrdering(com.webobjects.eocontrol.EOSortOrdering)
*/
@Override
public void addOrderByAttributeOrdering(EOSortOrdering sortOrdering) {
super.addOrderByAttributeOrdering(sortOrdering);
String nullSortBehavior = NSProperties.getProperty("EROraclePlugin.nullSortBehavior");
if (nullSortBehavior != null) {
if ("EOFStyle".equals(nullSortBehavior)) {
if (sortOrdering.selector() == EOSortOrdering.CompareCaseInsensitiveDescending || sortOrdering.selector() == EOSortOrdering.CompareDescending) {
_orderByString().append(" NULLS LAST");
}
else {
_orderByString().append(" NULLS FIRST");
}
}
else if ("NullsFirst".equals(nullSortBehavior)) {
_orderByString().append(" NULLS FIRST");
}
else if ("NullsLast".equals(nullSortBehavior)) {
_orderByString().append(" NULLS LAST"); // Oracle's normal default
}
}
}
}