package com.webobjects.jdbcadaptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOJoin; import com.webobjects.eoaccess.EOModel; import com.webobjects.eoaccess.EORelationship; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.eocontrol.EOSortOrdering; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSRange; import com.webobjects.foundation.NSSelector; import com.webobjects.foundation.NSTimestamp; import com.webobjects.foundation._NSStringUtilities; /** * Postgres needs special handling of NSData conversion, special * escape char, has a regex query selector and handles JOIN clauses correctly. * @author ak: Regex, NSData * @author Giorgio Valoti: refactoring, typecasting, schema building * @author Arturo Perez: JOIN clauses * @author David Teran: Timestamps handling * @author Tim Cummings: case sensitive table and column names * @author cug: hacks for identifier quoting while creating tables */ public class PostgresqlExpression extends JDBCExpression { /** * Selector used for case insensitive regular expressions. **/ // CHECKME AK: why is this public?? public static final NSSelector CASE_INSENSITIVE_REGEX_OPERATOR = new NSSelector( "~*", new Class[]{ Object.class }); /** * Selector used for case sensitive regular expressions. */ // CHECKME AK: why is this public?? public static final NSSelector REGEX_OPERATOR = new NSSelector( "~", new Class[]{ Object.class }); /** * Lookup table for conversion of bytes -> hex. */ private static final char HEX_VALUES[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** * SQL escape character */ private static final char SQL_ESCAPE_CHAR = '|'; /** * Quote character when using case sensitive queries. */ private static final String EXTERNAL_NAME_QUOTE_CHARACTER = "\""; /** * Formatter to use when handling date columns. Each thread has its own copy. */ private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("''yyyy-MM-dd''"); } }; /** * Formatter to use when handling timestamp columns. Each thread has its own copy. */ private static final ThreadLocal<SimpleDateFormat> TIMESTAMP_FORMATTER = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("''yyyy-MM-dd HH:mm:ss.SSS''"); } }; /** * Method to get the string value from a BigDecimals from. */ private static Method _bigDecimalToString = null; /** * If true, don't use typecasting, ie: 'some text'::varchar(255) */ private Boolean _disableTypeCasting = null; /** * If true, queries will be created by using */ private Boolean _disableBindVariables = null; /** * Holds array of join clauses. */ private NSMutableArray<JoinClause> _alreadyJoined = new NSMutableArray<>(); /** * Fetch spec limit ivar */ private int _fetchLimit; /** * Fetch spec range ivar */ private NSRange _fetchRange; private final NSSelector<NSRange> _fetchRangeSelector = new NSSelector<>("fetchRange"); private Boolean _enableIdentifierQuoting; private Boolean _enableBooleanQuoting; private Boolean _useLowercaseForCaseInsensitiveLike; /** * Overridden to remove the rtrim usage. The original implementation * will remove every trailing space from character based column which * should not be OK for Postgres. */ public PostgresqlExpression(EOEntity entity) { super(entity); if (useLowercaseForCaseInsensitiveLike()) { _upperFunctionName = "LOWER"; } String customFunctionName = customFunctionForStringComparison(); if(customFunctionName != null) { _upperFunctionName = customFunctionName; } } /** * Checks the system property * <code>com.webobjects.jdbcadaptor.PostgresqlExpression.enableBooleanQuoting</code> * to enable or disable quoting (default) of boolean items. */ private boolean enableBooleanQuoting() { if(_enableBooleanQuoting == null) { _enableBooleanQuoting = Boolean.getBoolean(getClass().getName() + ".enableBooleanQuoting") ? Boolean.TRUE : Boolean.FALSE; } return _enableBooleanQuoting.booleanValue(); } /** * Checks the system property * <code>com.webobjects.jdbcadaptor.PostgresqlExpression.enableIdentifierQuoting</code> * to enable or disable quoting (default) of schema names, table names and * field names. Required if names which are case sensitive or reserved words * or have special characters. */ private boolean enableIdentifierQuoting() { if(_enableIdentifierQuoting == null) { _enableIdentifierQuoting = Boolean.getBoolean(getClass().getName() + ".enableIdentifierQuoting") ? Boolean.TRUE : Boolean.FALSE; } return _enableIdentifierQuoting.booleanValue(); } /** * Overridden to fix an issue with NStimestamp classes and "T" value-typed attributes. */ @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 to not call the super implementation. * * @param leftName the table name on the left side of the clause * @param rightName the table name on the right side of the clause * @param semantic the join semantic */ @Override public void addJoinClause(String leftName, String rightName, int semantic) { assembleJoinClause(leftName, rightName, semantic); } /** * Overridden to construct a valid SQL92 JOIN clause as opposed to * the Oracle-like SQL the superclass produces. * * @param leftName the table name on the left side of the clause * @param rightName the table name on the right side of the clause * @param semantic the join semantic * @return the join clause */ @Override public String assembleJoinClause(String leftName, String rightName, int semantic) { if (!useAliases()) { return super.assembleJoinClause(leftName, rightName, semantic); } String leftAlias = leftName.substring(0, leftName.indexOf(".")); String rightAlias = rightName.substring(0, rightName.indexOf(".")); NSArray<String> k; EOEntity rightEntity; EOEntity leftEntity; String relationshipKey = null; EORelationship r; if (leftAlias.equals("t0")) { leftEntity = entity(); } else { k = aliasesByRelationshipPath().allKeysForObject(leftAlias); relationshipKey = k.count()>0? (String)k.lastObject() : ""; leftEntity = entityForKeyPath(relationshipKey); } if (rightAlias.equals("t0")) { rightEntity = entity(); } else { k = aliasesByRelationshipPath().allKeysForObject(rightAlias); relationshipKey = k.count()>0? (String)k.lastObject() : ""; rightEntity = entityForKeyPath(relationshipKey); } if (relationshipKey == null) { throw new IllegalStateException("Could not determine relationship for join."); } int dotIndex = relationshipKey.indexOf( "." ); relationshipKey = dotIndex == -1 ? relationshipKey : relationshipKey.substring( relationshipKey.lastIndexOf( "." ) + 1 ); r = rightEntity.anyRelationshipNamed( relationshipKey ); // fix from Michael Müller for the case Foo.fooBars.bar has a Bar.foo relationship (instead of Bar.foos) if( r == null || r.destinationEntity() != leftEntity ) { r = leftEntity.anyRelationshipNamed( relationshipKey ); } //timc 2006-02-26 IMPORTANT or quotes are ignored and mixed case field names won't work String rightTable; String leftTable; if(enableIdentifierQuoting()) { rightTable = rightEntity.valueForSQLExpression(this); leftTable = leftEntity.valueForSQLExpression(this); } else { rightTable = rightEntity.externalName(); leftTable = leftEntity.externalName(); } JoinClause jc = new JoinClause(); jc.setTable1(leftTable, leftAlias); switch (semantic) { case EORelationship.LeftOuterJoin: jc.op = " LEFT OUTER JOIN "; break; case EORelationship.RightOuterJoin: jc.op = " RIGHT OUTER JOIN "; break; case EORelationship.FullOuterJoin: jc.op = " FULL OUTER JOIN "; break; case EORelationship.InnerJoin: jc.op = " INNER JOIN "; break; } jc.table2 = rightTable + " " + rightAlias; NSArray<EOJoin> joins = r.joins(); int joinsCount = joins.count(); NSMutableArray<String> joinStrings = new NSMutableArray<>(joinsCount); for( int i = 0; i < joinsCount; i++ ) { EOJoin currentJoin = joins.objectAtIndex(i); String left; String right; if(enableIdentifierQuoting()) { left = leftAlias +"."+ sqlStringForSchemaObjectName(currentJoin.sourceAttribute().columnName()); right = rightAlias +"."+ sqlStringForSchemaObjectName(currentJoin.destinationAttribute().columnName()); } else { left = leftAlias +"."+currentJoin.sourceAttribute().columnName(); right = rightAlias +"."+currentJoin.destinationAttribute().columnName(); } joinStrings.addObject( left + " = " + right); } jc.joinCondition = " ON " + joinStrings.componentsJoinedByString( " AND " ); if( !_alreadyJoined.containsObject( jc ) ) { _alreadyJoined.insertObjectAtIndex(jc, 0); return jc.toString(); } return null; } /** * Overridden to handle correct placements of join conditions and * to handle DISTINCT fetches with compareCaseInsensitiveA(De)scending sort orders. * * @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*", ""); 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); } // fetchRange overrides fetchLimit if (_fetchRange != null) { sb.append(" LIMIT "); sb.append(_fetchRange.length()); sb.append(" OFFSET "); sb.append(_fetchRange.location()); } else if (_fetchLimit != 0) { sb.append(" LIMIT "); sb.append(_fetchLimit); } return sb.toString(); } /** * Overrides the parent implementation to provide support * for array data types. * * @param attribute the EOattribute * @return the PostgreSQL specific type string for <code>attribute</code> */ @Override public String columnTypeStringForAttribute( EOAttribute attribute ) { String externalType = attribute.externalType(); if (externalType != null && externalType.endsWith( "[]" ) ) { return externalType; } //CHECKME: Why isn't this found in jdbcinfo? if("integer".equals(externalType)) { return externalType; } return super.columnTypeStringForAttribute( attribute ); } /** * Utility that traverses a key path to find the last destination entity * * @param keyPath the key path * @return the entity at the end of the keypath */ private EOEntity entityForKeyPath(String keyPath) { NSArray<String> keys = NSArray.componentsSeparatedByString(keyPath, "."); EOEntity ent = entity(); for (int i = 0; i < keys.count(); i++) { String k = keys.objectAtIndex(i); EORelationship rel = ent.anyRelationshipNamed(k); if (rel == null) { // it may be an attribute if (ent.anyAttributeNamed(k) != null) { break; } throw new IllegalArgumentException("relationship " + keyPath + " generated null"); } ent = rel.destinationEntity(); } return ent; } /** * Overridden because the original version throws when the * data contains negative byte values. * * @param obj the object used in the SQL statement * @param eoattribute the attribute associated with <code>obj</code> * @return the formatted string */ @Override public String formatValueForAttribute(Object obj, EOAttribute eoattribute) { String value; if(obj instanceof NSData) { value = sqlStringForData((NSData)obj); } else if((obj instanceof NSTimestamp) && isTimestampAttribute(eoattribute)) { value = TIMESTAMP_FORMATTER.get().format(obj); } else if((obj instanceof NSTimestamp) && isDateAttribute(eoattribute)) { value = DATE_FORMATTER.get().format(obj); } else if(obj instanceof String) { value = formatStringValue((String)obj); } else if(obj instanceof Number) { if(obj instanceof BigDecimal) { value = fixBigDecimal((BigDecimal) obj, eoattribute); } else { Object convertedObj = eoattribute.adaptorValueByConvertingAttributeValue(obj); if (convertedObj instanceof Number) { String valueType = eoattribute.valueType(); if (valueType == null || "i".equals(valueType)) { value = String.valueOf(((Number)convertedObj).intValue()); } else if ("l".equals(valueType)) { value = String.valueOf(((Number)convertedObj).longValue()); } else if ("f".equals(valueType)) { value = String.valueOf(((Number)convertedObj).floatValue()); } else if ("d".equals(valueType)) { value = String.valueOf(((Number)convertedObj).doubleValue()); } else if ("s".equals(valueType)) { value = String.valueOf(((Number)convertedObj).shortValue()); } else if ("c".equals(valueType)) { return String.valueOf(((Number)convertedObj).intValue()); } else { throw new IllegalArgumentException("Unknown number value type '" + valueType + "' for attribute " + eoattribute.entity().name() + "." + eoattribute.name() + "."); //value = convertedObj.toString(); } } else if (convertedObj instanceof String) { String str = (String)convertedObj; String valueType = eoattribute.valueType(); if (valueType == null || "i".equals(valueType)) { return String.valueOf(Integer.parseInt(str)); } else if ("l".equals(valueType)) { return String.valueOf(Long.parseLong(str)); } else if ("f".equals(valueType)) { return String.valueOf(Float.parseFloat(str)); } else if ("d".equals(valueType)) { return String.valueOf(Double.parseDouble(str)); } else if ("s".equals(valueType)) { return String.valueOf(Short.parseShort(str)); } else if ("c".equals(valueType)) { return String.valueOf(Integer.parseInt(str)); } else { throw new IllegalArgumentException("Unknown number value type '" + valueType + "' for attribute " + eoattribute.entity().name() + "." + eoattribute.name() + "."); } } else { throw new IllegalArgumentException("Unknown number value '" + obj + "' for attribute " + eoattribute.entity().name() + "." + eoattribute.name() + "."); } } } else if(obj instanceof Boolean) { // GN: when booleans are stored as strings in the db, we need the values quoted if (enableBooleanQuoting() || "S".equals(eoattribute.valueType())) { value = "'" + ((Boolean)obj).toString() + "'"; } else if (!"bool".equals(eoattribute.externalType().toLowerCase()) && "NSNumber".equals(eoattribute.valueClassName()) || "java.lang.Number".equals(eoattribute.valueClassName()) || "Number".equals(eoattribute.valueClassName())) { value = ((Boolean)obj).booleanValue() ? "1" : "0"; } else { value = ((Boolean)obj).toString(); } } else if(obj instanceof Timestamp) { value = "'" + ((Timestamp)obj).toString() + "'"; } else if (obj == null || obj == NSKeyValueCoding.NullValue) { value = "NULL"; } else { // AK: I don't really like this, but we might want to prevent infinite recursion try { Object adaptorValue = eoattribute.adaptorValueByConvertingAttributeValue(obj); if(adaptorValue instanceof Date && !(adaptorValue instanceof NSTimestamp)) { //Support joda classes Date date = (Date)adaptorValue; adaptorValue = new NSTimestamp(date); } if(adaptorValue instanceof NSData || adaptorValue instanceof NSTimestamp || adaptorValue instanceof String || adaptorValue instanceof Number || adaptorValue instanceof Boolean) { value = formatValueForAttribute(adaptorValue, eoattribute); } else { throw new IllegalArgumentException(getClass().getName() + ": Can't convert: " + obj + ":" + obj.getClass() + " -> " + adaptorValue + ":" +adaptorValue.getClass()); } } catch(Exception ex) { throw new IllegalArgumentException(getClass().getName() + ": Exception while converting " + obj.getClass().getName(), ex); } } return value; } /** * Fixes an incompatibility with JDK 1.5 and using toString() instead of toPlainString() for BigDecimals. * From what I understand, you will only need this if you disable bind variables. * @param value * @param eoattribute * @author ak */ private String fixBigDecimal(BigDecimal value, EOAttribute eoattribute) { String result; if(System.getProperty("java.version").compareTo("1.5") >= 0) { try { if(_bigDecimalToString == null) { _bigDecimalToString = BigDecimal.class.getMethod("toPlainString", (Class[])null); } result = (String) _bigDecimalToString.invoke(value, (Object[])null); } catch (IllegalArgumentException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (IllegalAccessException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (InvocationTargetException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (SecurityException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } catch (NoSuchMethodException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } else { result = value.toString(); } return result; } /** * Helper to check for timestamp columns that have a "D" value type. * @param eoattribute attribute to check * @return <code>true</code> if of type Date */ private boolean isDateAttribute(EOAttribute eoattribute) { return "D".equals(eoattribute.valueType()); } /** * Helper to check for timestamp columns that have a "T" value type. * @param eoattribute attribute to check * @return <code>true</code> if of type Timestamp */ private boolean isTimestampAttribute(EOAttribute eoattribute) { return "T".equals(eoattribute.valueType()); } /** * Helper to check for data columns that are not keys. * @param eoattribute attribute to check * @return <code>true</code> if of type Data */ private boolean isDataAttribute(EOAttribute attribute) { return (attribute.className().equals("com.webobjects.foundation.NSData") || attribute.externalType().equals("bytea") || attribute.externalType().equals("bit")) && entity().classProperties().containsObject(attribute); } /** * Overrides the parent implementation to compose the final string * expression for the join clauses. */ @Override public String joinClauseString() { NSMutableDictionary<String, Boolean> seenIt = new NSMutableDictionary<>(); StringBuilder sb = new StringBuilder(); JoinClause jc; EOSortOrdering.sortArrayUsingKeyOrderArray ( _alreadyJoined, new NSArray<>( EOSortOrdering.sortOrderingWithKey( "sortKey", EOSortOrdering.CompareCaseInsensitiveAscending ) ) ); if (_alreadyJoined.count() > 0) { jc = _alreadyJoined.objectAtIndex(0); sb.append(jc); seenIt.setObjectForKey(Boolean.TRUE, jc.table1); seenIt.setObjectForKey(Boolean.TRUE, jc.table2); } for (int i = 1; i < _alreadyJoined.count(); i++) { jc = _alreadyJoined.objectAtIndex(i); sb.append(jc.op); if (seenIt.objectForKey(jc.table1) == null) { sb.append(jc.table1); seenIt.setObjectForKey(Boolean.TRUE, jc.table1); } else if (seenIt.objectForKey(jc.table2) == null) { sb.append(jc.table2); seenIt.setObjectForKey(Boolean.TRUE, jc.table2); } sb.append(jc.joinCondition); } return sb.toString(); } /** * Overrides the parent implementation to add an <code>INITIALLY DEFERRED</code> to the generated statement. * Useful you want to generate the schema-building SQL from a pure java environment. * * cug: Also handles identifier quoting now * * @param relationship the relationship * @param sourceColumns the source columns for the constraints * @param destinationColumns the destination columns for the constraints */ @SuppressWarnings("unchecked") @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 = _NSStringUtilities.concat(tableName, "_", relationship.name(), "_fk"); // quotes the identifier in the array String sourceKeyList = quoteArrayContents(sourceColumns).componentsJoinedByString(", "); String destinationKeyList = quoteArrayContents(destinationColumns).componentsJoinedByString(", "); EOModel sourceModel = entity.model(); EOModel destModel = relationship.destinationEntity().model(); if (sourceModel != destModel && !sourceModel.connectionDictionary().equals(destModel.connectionDictionary())) { throw new IllegalArgumentException(new StringBuilder().append("prepareConstraintStatementForRelationship unable to create a constraint for ").append(relationship.name()).append(" because the source and destination entities reside in different databases").toString()); } setStatement(new StringBuilder() .append("ALTER TABLE ") .append(sqlStringForSchemaObjectName(entity.externalName())) .append(" ADD CONSTRAINT ") .append(quoteIdentifier(constraintName)) .append(" FOREIGN KEY (") .append(sourceKeyList) .append(") REFERENCES ") .append(sqlStringForSchemaObjectName(relationship.destinationEntity().externalName())) .append(" (") .append(destinationKeyList) .append(") INITIALLY DEFERRED") .toString()); } /** * Takes an array of strings and quotes every single string, if set to do so * * @param a - array of strings * * @return array of quoted or unquoted strings, depends on enableIdentifierQuoting */ private NSArray<String> quoteArrayContents(NSArray<String> a) { Enumeration enumeration = a.objectEnumerator(); NSMutableArray<String> result = new NSMutableArray<>(); while (enumeration.hasMoreElements()) { String identifier = (String) enumeration.nextElement(); String quotedString = quoteIdentifier(identifier); result.addObject(quotedString); } return result; } /** * Quotes the string if necessary (checks the Property) * * @param identifier - the string to quote * * @return quoted or unquoted string (check with enableIdentifierQuoting) */ private String quoteIdentifier(String identifier) { return externalNameQuoteCharacter() + identifier + externalNameQuoteCharacter(); } /** * Overridden so we can get the fetch limit from the fetchSpec. * * @param attributes the array of attributes * @param lock locking flag * @param fetchSpec the fetch specification */ @Override public void prepareSelectExpressionWithAttributes(NSArray<EOAttribute> attributes, boolean lock, EOFetchSpecification fetchSpec) { try { _fetchRange = _fetchRangeSelector.invoke(fetchSpec); // We will get an error when not using our custom ERXFetchSpecification subclass // We could have added ERExtensions to the classpath and checked for instanceof, but I thought // this is a little cleaner since people may be using this PlugIn and not Wonder in some legacy apps. } catch (IllegalArgumentException e) { // ignore } catch (IllegalAccessException e) { // ignore } catch (InvocationTargetException e) { // ignore } catch (NoSuchMethodException e) { // ignore } // Only check for fetchLimit if fetchRange is not provided. if (_fetchRange == null && !fetchSpec.promptsAfterFetchLimit()) { _fetchLimit = fetchSpec.fetchLimit(); } if (_fetchRange != null) { // if we have a fetch range disable the limit fetchSpec.setFetchLimit(0); } super.prepareSelectExpressionWithAttributes(attributes, lock, fetchSpec); } /** * Overridden because Postgres uses "|" instead of "\" like any * other database system. */ @Override public char sqlEscapeChar() { return SQL_ESCAPE_CHAR; } /** * Overridden because PostgreSQL does not use the default quote character in EOSQLExpression.externalNameQuoteCharacter() which is an empty string. * */ @Override public String externalNameQuoteCharacter() { return (enableIdentifierQuoting() ? EXTERNAL_NAME_QUOTE_CHARACTER : ""); } 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 && parentEntity != null) { // If you have a parent entity and that parent entity shares your table name, then you're single table inheritance boolean singleTableInheritance = 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 allowsNullClauseForConstraint = allowsNullClauseForConstraint(shouldAllowNull(attribute)); String sql; if (defaultValue == null) { sql = _NSStringUtilities.concat(quoteIdentifier(attribute.columnName()), " ", columnTypeStringForAttribute(attribute), " ", allowsNullClauseForConstraint); } else { sql = _NSStringUtilities.concat(quoteIdentifier(attribute.columnName()), " ", columnTypeStringForAttribute(attribute), " DEFAULT ", formatValueForAttribute(defaultValue, attribute), " ", allowsNullClauseForConstraint); } appendItemToListString(sql, _listString()); } /** * cug: Quick hack for bug in WebObjects 5.4 where the "not null" statement is added without a space, * and "addCreateClauseForAttribute" is not called anymore. Will probably change. */ @Override public String allowsNullClauseForConstraint(boolean allowsNull) { if(allowsNull) return ""; Object value = jdbcInfo().objectForKey("NON_NULLABLE_COLUMNS"); if(value != null && value.equals("T")) return " NOT NULL"; return ""; } /** * Overridden because the original version does not correctly quote mixed case fields in all cases. * SELECT statements were OK (useAliases is true) INSERT, UPDATE, DELETE didn't quote mixed case field names. * * @param attribute the attribute (column name) to be converted to a SQL string * @return SQL string for the attribute */ @Override public String sqlStringForAttribute(EOAttribute attribute) { String sql = null; if ( attribute.isDerived() || useAliases() || attribute.columnName() == null || !enableIdentifierQuoting()) { sql = super.sqlStringForAttribute(attribute); } else { sql = sqlStringForSchemaObjectName(attribute.columnName()); } //NSLog.out.appendln("PostgresqlExpression.sqlStringForAttribute " + attribute.columnName() + ", isDerived() = " + attribute.isDerived() + ", useAliases() = " + useAliases() + ", sql = " + sql); return sql; } /** * Overridden because the original version does not correctly quote mixed case table names in all cases. * SELECT statements were OK (useAliases is true) INSERT, UPDATE, DELETE didn't quote mixed case field names. * * @return the SQL string for the table names */ @Override public String tableListWithRootEntity(EOEntity entity) { String sql = null; if ( useAliases()) { sql = super.tableListWithRootEntity(entity); } else { sql = entity.valueForSQLExpression(this); } //NSLog.out.appendln("PostgresqlExpression.tableListWithRootEntity " + entity.externalName() + ", useAliases() = " + useAliases() + ", sql = " + sql); return sql; } /** * Overridden because the original version throws an exception when the * data contains negative byte values. * This method is only for smaller values like binary primary keys or such. * * @param data the data to be converted to a SQL string * @return the SQL string for raw data */ @Override public String sqlStringForData(NSData data) { int length = data.length(); byte bytes[] = data.bytes(); char hex[] = new char[2 * length]; int nibbles = 0; for(int i = 0; i < length; i++) { byte b = bytes[i]; hex[nibbles++] = HEX_VALUES[((b >> 4) + 16) % 16]; hex[nibbles++] = HEX_VALUES[((b & 15) + 16) % 16]; } return "decode('" + new String(hex) + "','hex')"; } /** * Overridden so we can put a regex-match qualifier in the display groups * query bindings. You can bind '~*' or '~' to queryOperator.someKey and '.*foo' to * queryMatch.someKey and will get the correct results. * * @param selector the selector that specifies the SQL operator * @param value the value to be associated with <code>selector</code> * @return the SQL operator string */ @Override public String sqlStringForSelector(NSSelector selector, Object value) { if(CASE_INSENSITIVE_REGEX_OPERATOR.name().equals( selector.name() ) || REGEX_OPERATOR.name().equals( selector.name() ) ) { return selector.name(); } return super.sqlStringForSelector(selector, value); } /** * Overrides the parent implementation to: * <ul> * <li>add typecasts after the value, i.e. '2'::char, * which is required with certain PostgreSQL versions (<=7.4.x) for the correct query processing, * particularly with index usage. * <li>quotes values if bind variables are disabled on this attribute. * </ul> * NULL values are excluded from casting. * <p> * You can set the System default <code>com.webobjects.jdbcadaptor.PostgresqlExpression.disableTypeCasting</code> * to true to disable both fixes (the former you might want to disable when PG says it can't cast a certain value and * the second when you have values with a greater resolution already in the DB). * @param v the value * @param kp the keypath associated with the value */ @Override public String sqlStringForValue(Object v, String kp) { if(disableTypeCasting()) { return super.sqlStringForValue(v,kp); } EOAttribute attribute; int lastDotIdx = kp.lastIndexOf("."); if (lastDotIdx == -1) { attribute = entity().attributeNamed(kp); } else { EOEntity kpEntity = entityForKeyPath(kp); attribute = kpEntity.attributeNamed(kp.substring(lastDotIdx+1)); } // AK: inet6 addresses get handed down as "xxx:xxx:...:xxx%y", not "xxx:xxx:...:xxx/y" // note that this might break if you hand over a host name that contains percent chars (not sure if possible) if(attribute != null && "inet".equals(attribute.externalType()) && v != null && v.toString().indexOf('%') > 0) { v = v.toString().replace('%', '/'); } if(attribute != null && v != null && v != NSKeyValueCoding.NullValue) { String s = columnTypeStringForAttribute(attribute); return super.sqlStringForValue(v, kp) + "::" + s; } return super.sqlStringForValue(v,kp); } /** * Helper class that stores a join definition and * helps <code>PostgresqlExpression</code> to assemble * the correct join clause. */ public static class JoinClause { String table1; String op; String table2; String joinCondition; String sortKey; @Override public String toString() { return table1 + op + table2 + joinCondition; } @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof JoinClause)) { return false; } return toString().equals(obj.toString()); } public void setTable1(String leftTable, String leftAlias) { table1 = leftTable + " " + leftAlias; sortKey = leftAlias.substring(1); if (sortKey.length() < 2) { // add padding for cases with >9 joins sortKey = " " + sortKey; } } /** * Property that makes this class "sortable". * Needed to correctly assemble a join clause. */ public String sortKey() { return sortKey; } } /** * Checks the system property <code>com.webobjects.jdbcadaptor.PostgresqlExpression.disableTypeCasting</code> to enable or * disable typecasting (appending ::somepostgrestype) for attributes. */ private boolean disableTypeCasting() { if (_disableTypeCasting == null) { _disableTypeCasting = Boolean.getBoolean("com.webobjects.jdbcadaptor.PostgresqlExpression.disableTypeCasting") ? Boolean.TRUE : Boolean.FALSE; } return _disableTypeCasting.booleanValue(); } /** * Checks the system property <code>com.webobjects.jdbcadaptor.PostgresqlExpression.disableBindVariables</code> to enable * or disable bind variables in general. */ private boolean disableBindVariables() { if (_disableBindVariables == null) { _disableBindVariables = Boolean.getBoolean("com.webobjects.jdbcadaptor.PostgresqlExpression.disableBindVariables") ? Boolean.TRUE : Boolean.FALSE; } return _disableBindVariables.booleanValue(); } /** * Overridden to return the negated value of {@link #disableBindVariables()}. */ @Override public boolean useBindVariables() { return !disableBindVariables(); } /** * Overridden to set the <code>disableBindVariables</code> value correctly. * @param value */ @Override public void setUseBindVariables(boolean value) { _disableBindVariables = Boolean.valueOf(!value); } /** * Overridden to return true only if bind variables are enabled or the is a data type. */ @Override public boolean shouldUseBindVariableForAttribute(EOAttribute attribute) { return useBindVariables() || isDataAttribute(attribute); } /** * Overridden to return true only if bind variables are enabled or the is a data type. */ @Override public boolean mustUseBindVariableForAttribute(EOAttribute attribute) { return useBindVariables() || isDataAttribute(attribute); } /** * Replaces a given string by another string in a string. * @param old string to be replaced * @param newString to be inserted * @param buffer string to have the replacement done on it * @return string after having all of the replacement done. * @deprecated use {@link StringUtils#replace(String, String, String)} instead */ public static String replaceStringByStringInString(String old, String newString, String buffer) { int begin, end; int oldLength = old.length(); int length = buffer.length(); StringBuilder convertedString = new StringBuilder(length + 100); begin = 0; while(begin < length) { end = buffer.indexOf(old, begin); if(end == -1) { convertedString.append(buffer.substring(begin)); break; } if(end == 0) convertedString.append(newString); else { convertedString.append(buffer.substring(begin, end)); convertedString.append(newString); } begin = end+oldLength; } return convertedString.toString(); } /** * Checks the system property * <code>com.webobjects.jdbcadaptor.PostgresqlExpression.useLowercaseForCaseInsensitiveLike</code> * to use the "lower" function for caseInsensitive compares */ private boolean useLowercaseForCaseInsensitiveLike() { if (_useLowercaseForCaseInsensitiveLike == null) { _useLowercaseForCaseInsensitiveLike = Boolean.getBoolean(getClass().getName() + ".useLowercaseForCaseInsensitiveLike") ? Boolean.TRUE : Boolean.FALSE; } return _useLowercaseForCaseInsensitiveLike.booleanValue(); } /** * Checks the system property * <code>com.webobjects.jdbcadaptor.PostgresqlExpression.customFunctionForStringComparison</code> * to use a custom function for caseInsensitive compares and order by * clauses. * <p> * This property overrides the useLowercaseForCaseInsensitiveLike definition. * * @return The name of the custom function to be used for comparison or <code>null</code> to use * the default function */ private String customFunctionForStringComparison() { return System.getProperty(getClass().getName() + ".customFunctionForStringComparison"); } }