package com.webobjects.jdbcadaptor; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.sql.Blob; import java.sql.Clob; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.Enumeration; import com.webobjects.eoaccess.EOAdaptor; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOJoin; import com.webobjects.eoaccess.EORelationship; import com.webobjects.eoaccess.EOSQLExpression; import com.webobjects.eoaccess.synchronization.EOSchemaGenerationOptions; import com.webobjects.eoaccess.synchronization.EOSchemaSynchronization; import com.webobjects.eoaccess.synchronization.EOSchemaSynchronizationFactory; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.eocontrol.EOSortOrdering; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSBundle; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSLog; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSMutableSet; import com.webobjects.foundation.NSPropertyListSerialization; import com.webobjects.foundation.NSRange; import com.webobjects.foundation.NSSelector; import com.webobjects.foundation.NSTimestamp; import com.webobjects.foundation._NSStringUtilities; /** * WO runtime plugin with support for H2. * * @see <a href="http://www.h2database.com/">http://www.h2database.com</a> */ public class _H2PlugIn extends JDBCPlugIn { static final boolean USE_NAMED_CONSTRAINTS = true; protected static NSMutableDictionary<String, String> sequenceNameOverrides = new NSMutableDictionary<String, String>(); /** * 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"); } }; protected static String quoteTableName(String name) { String result = null; if (name != null) { int i = name.lastIndexOf(46); if (i < 0) { result = new StringBuilder().append('"').append(name).append('"').toString(); } else { result = new StringBuilder(name.substring(0, i)) .append("\".\"") .append(name.substring(i + 1, name.length())) .append('"') .toString(); } } return result; } static String singleQuotedString(Object value) { return value == null ? null : singleQuotedString(value.toString()); } static String singleQuotedString(String string) { if (string == null) { return null; } return new StringBuilder().append('\'').append(string).append('\'').toString(); } /** * Utility method that returns the name of the sequence associated * with the given <code>entity</code>. * * @param entity the entity * @return the name of the sequence */ protected static String _sequenceNameForEntity(EOEntity entity) { String sequenceName = entity.primaryKeyRootName() + "_seq"; synchronized (sequenceNameOverrides) { if (sequenceNameOverrides.containsKey(sequenceName)) { sequenceName = sequenceNameOverrides.get(sequenceName); } } return sequenceName; } /** * Sets the sequence name to be used in H2 instead of the default WO sequence name. * This is needed if the sequence has been created outside of WO and its name * is differing from the default WO schema as H2 does not yet support renaming * of sequences. * * @param defaultName WO default sequence name * @param h2Name sequence name in H2 */ protected static void setSequenceNameOverride(String defaultName, String h2Name) { synchronized (sequenceNameOverrides) { sequenceNameOverrides.put(defaultName, h2Name); } } @Override public Object fetchBLOB(ResultSet rs, int column, EOAttribute attribute, boolean materialize) throws SQLException { NSData data = null; Blob blob = rs.getBlob(column); if(blob == null) { return null; } if(!materialize) { return blob; } InputStream stream = blob.getBinaryStream(); try { int chunkSize = (int)blob.length(); if(chunkSize == 0) { data = NSData.EmptyData; } else { data = new NSData(stream, chunkSize); } } catch(IOException e) { throw new JDBCAdaptorException(e.getMessage(), null); } finally { try {if(stream != null) stream.close(); } catch(IOException e) { /* Nothing we can do */ }; } return data; } @Override public Object fetchCLOB(ResultSet rs, int column, EOAttribute attribute, boolean materialize) throws SQLException { Clob clob = rs.getClob(column); if (clob == null) { return null; } if (!materialize) { return clob; } else { return clob.getSubString(1L, (int) clob.length()); } } public static class H2Expression extends JDBCExpression { /** * Holds array of join clauses. */ private NSMutableArray<JoinClause> _alreadyJoined = new NSMutableArray<JoinClause>(); /** * Fetch spec limit ivar */ private int _fetchLimit; /** * Fetch spec range ivar */ private NSRange _fetchRange; private final NSSelector<NSRange> _fetchRangeSelector = new NSSelector<NSRange>("fetchRange"); public H2Expression(final EOEntity entity) { super(entity); } @Override public void addCreateClauseForAttribute(final EOAttribute attribute) { StringBuilder sql = new StringBuilder(); sql.append(attribute.columnName()); sql.append(' '); sql.append(columnTypeStringForAttribute(attribute)); NSDictionary<String, Object> userInfo = attribute.userInfo(); if (userInfo != null) { Object defaultValue = userInfo.valueForKey("er.extensions.eoattribute.default"); // deprecated key if (defaultValue == null) { defaultValue = userInfo.valueForKey("default"); } if (defaultValue != null) { sql.append(" DEFAULT "); sql.append(formatValueForAttribute(defaultValue, attribute)); } } sql.append(' '); sql.append(allowsNullClauseForConstraint(attribute.allowsNull())); appendItemToListString(sql.toString(), _listString()); } protected boolean enableBooleanQuoting() { return false; } /** * @param value * @param eoattribute * @return the plain string representation of the given value */ private String formatBigDecimal(final BigDecimal value, final EOAttribute eoattribute) { return value.toPlainString(); } @Override public String formatValueForAttribute(final Object value, final EOAttribute eoattribute) { String result; if (value instanceof NSData) { result = sqlStringForData((NSData) value); } else if (value instanceof NSTimestamp && isTimestampAttribute(eoattribute)) { result = singleQuotedString(TIMESTAMP_FORMATTER.get().format(value)); } else if (value instanceof NSTimestamp && isDateAttribute(eoattribute)) { result = singleQuotedString(DATE_FORMATTER.get().format(value)); } else if (value instanceof String) { result = formatStringValue((String) value); } else if (value instanceof Number) { if (value instanceof BigDecimal) { result = formatBigDecimal((BigDecimal) value, eoattribute); } else { Object convertedValue = eoattribute.adaptorValueByConvertingAttributeValue(value); if (convertedValue instanceof Number) { Number convertedNumberValue = (Number) convertedValue; String valueType = eoattribute.valueType(); if (valueType == null || "i".equals(valueType)) { result = String.valueOf(convertedNumberValue.intValue()); } else if ("l".equals(valueType)) { result = String.valueOf(convertedNumberValue.longValue()); } else if ("f".equals(valueType)) { result = String.valueOf(convertedNumberValue.floatValue()); } else if ("d".equals(valueType)) { result = String.valueOf(convertedNumberValue.doubleValue()); } else if ("s".equals(valueType)) { result = String.valueOf(convertedNumberValue.shortValue()); } else { result = convertedNumberValue.toString(); } } else { result = convertedValue.toString(); } } } else if (value instanceof Boolean) { // GN: when booleans are stored as strings in the db, we need // the values quoted if (enableBooleanQuoting()) { result = singleQuotedString(value); } else { result = value.toString(); } } else if (value instanceof Timestamp) { result = singleQuotedString(value); } else if (value == null || value == NSKeyValueCoding.NullValue) { result = "NULL"; } else { // AK: I don't really like this, but we might want to prevent // infinite recursion try { Object adaptorValue = eoattribute.adaptorValueByConvertingAttributeValue(value); if (adaptorValue instanceof NSData || adaptorValue instanceof NSTimestamp || adaptorValue instanceof String || adaptorValue instanceof Number || adaptorValue instanceof Boolean) { result = formatValueForAttribute(adaptorValue, eoattribute); } else { StringBuilder buff = new StringBuilder(getClass().getName()) .append(": Can't convert: ") .append(value) .append(':') .append(value.getClass().getName()) .append(" -> ") .append(adaptorValue) .append(':') .append(adaptorValue.getClass().getName()); NSLog.err.appendln(buff.toString()); result = value.toString(); } } catch (Exception ex) { StringBuilder buff = new StringBuilder(getClass().getName()) .append(": Exception while converting ") .append(value.getClass().getName()); NSLog.err.appendln(buff.toString()); NSLog.err.appendln(ex); result = value.toString(); } } return result; } /** * Helper to check for timestamp columns that have a "D" value type. * * @param eoattribute */ private boolean isDateAttribute(final EOAttribute eoattribute) { return eoattribute != null && "D".equals(eoattribute.valueType()); } /** * Helper to check for timestamp columns that have a "T" value type. * * @param eoattribute */ private boolean isTimestampAttribute(final EOAttribute eoattribute) { return eoattribute != null && "T".equals(eoattribute.valueType()); } /** * 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 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(); } /** * 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; } /** * Helper class that stores a join definition and * helps <code>H2Expression</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; } } /** * Overrides the parent implementation to compose the final string * expression for the join clauses. */ @Override public String joinClauseString() { NSMutableDictionary<String, Boolean> seenIt = new NSMutableDictionary<String, Boolean>(); StringBuilder sb = new StringBuilder(); JoinClause jc; EOSortOrdering.sortArrayUsingKeyOrderArray ( _alreadyJoined, new NSArray<EOSortOrdering>( 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(); } /** * 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()) { System.out.println("_H2PlugIn.H2Expression.assembleJoinClause: calling super!"); 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 ); } String rightTable = rightEntity.externalName(); String 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<String>(joinsCount); for( int i = 0; i < joinsCount; i++ ) { EOJoin currentJoin = joins.objectAtIndex(i); String left = leftAlias +"."+currentJoin.sourceAttribute().columnName(); String 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; } } public static class H2SynchronizationFactory extends EOSchemaSynchronizationFactory { public H2SynchronizationFactory(final EOAdaptor adaptor) { super(adaptor); } @Override public NSArray<EOSQLExpression> _statementsToDropPrimaryKeyConstraintsOnTableNamed(String tableName) { return new NSArray<EOSQLExpression>(_expressionForString("ALTER TABLE " + formatTableName(tableName) + " DROP PRIMARY KEY")); } public String columnTypeStringForAttribute(EOAttribute attribute) { if (attribute.precision() != 0) { String precision = String.valueOf(attribute.precision()); String scale = String.valueOf(attribute.scale()); return _NSStringUtilities.concat(attribute.externalType(), "(", precision, ",", scale, ")"); } if (attribute.width() != 0) { String width = String.valueOf(attribute.width()); return _NSStringUtilities.concat(attribute.externalType(), "(", width, ")"); } return attribute.externalType(); } @Override public NSArray<EOSQLExpression> dropPrimaryKeySupportStatementsForEntityGroup(NSArray<EOEntity> entityGroup) { NSMutableSet<String> sequenceNames = new NSMutableSet<String>(entityGroup.count()); NSMutableArray<EOSQLExpression> results = new NSMutableArray<EOSQLExpression>(); for (EOEntity entity : entityGroup) { String sequenceName = H2PlugIn._sequenceNameForEntity(entity); if (!sequenceNames.containsObject(sequenceName)) { sequenceNames.addObject(sequenceName); String sql = "DROP SEQUENCE " + sequenceName; results.addObject(createExpression(entity, sql)); } } return results; } @Override public NSArray<EOSQLExpression> dropTableStatementsForEntityGroup(NSArray<EOEntity> entityGroup) { String tableName = entityGroup.objectAtIndex(0).externalName(); return new NSArray<EOSQLExpression>(_expressionForString("DROP TABLE " + formatTableName(tableName))); } public String formatUpperString(String string) { return string.toUpperCase(); } boolean isPrimaryKeyAttributes(EOEntity entity, NSArray<EOAttribute> attributes) { NSArray<String> keys = entity.primaryKeyAttributeNames(); boolean result = attributes.count() == keys.count(); if (result) { for (int i = 0; i < keys.count(); i++) { if (!(result = keys.indexOfObject(attributes.objectAtIndex(i).name()) != NSArray.NotFound)) break; } } return result; } @Override public NSArray<EOSQLExpression> foreignKeyConstraintStatementsForRelationship(EORelationship relationship) { if (relationship != null && !relationship.isToMany() && isPrimaryKeyAttributes(relationship.destinationEntity(), relationship.destinationAttributes())) { StringBuilder sql = new StringBuilder(); String tableName = formatTableName(relationship.entity().externalName()); sql.append("ALTER TABLE "); sql.append(tableName); sql.append(" ADD"); StringBuilder constraint = new StringBuilder(" CONSTRAINT \"FOREIGN_KEY_"); constraint.append(formatUpperString(tableName)); StringBuilder fkSql = new StringBuilder(" FOREIGN KEY ("); NSArray<EOAttribute> attributes = relationship.sourceAttributes(); for (int i = 0; i < attributes.count(); i++) { constraint.append('_'); if (i != 0) fkSql.append(", "); String columnName = formatColumnName(attributes.objectAtIndex(i).columnName()); fkSql.append(columnName); constraint.append(formatUpperString(columnName)); } fkSql.append(") REFERENCES "); constraint.append('_'); String referencedExternalName = formatTableName(relationship.destinationEntity().externalName()); fkSql.append(referencedExternalName); constraint.append(formatUpperString(referencedExternalName)); fkSql.append(" ("); attributes = relationship.destinationAttributes(); for (int i = 0; i < attributes.count(); i++) { constraint.append('_'); if (i != 0) fkSql.append(", "); String referencedColumnName = formatColumnName(attributes.objectAtIndex(i).columnName()); fkSql.append(referencedColumnName); constraint.append(formatUpperString(referencedColumnName)); } // MS: did i write this code? sorry about that everything. this is crazy. constraint.append('"'); fkSql.append(')'); // BOO //fkSql.append(") DEFERRABLE INITIALLY DEFERRED"); if (USE_NAMED_CONSTRAINTS) sql.append(constraint); sql.append(fkSql); return new NSArray<EOSQLExpression>(_expressionForString(sql.toString())); } return NSArray.EmptyArray; } @Override public NSArray<EOSQLExpression> primaryKeySupportStatementsForEntityGroup(NSArray<EOEntity> entityGroup) { NSMutableSet<String> sequenceNames = new NSMutableSet<String>(); NSMutableArray<EOSQLExpression> results = new NSMutableArray<EOSQLExpression>(); for (EOEntity entity : entityGroup) { if (isPrimaryKeyGenerationNotSupported(entity)) { continue; } EOAttribute priKeyAttribute = entity.primaryKeyAttributes().objectAtIndex(0); String sql; String sequenceName = H2PlugIn._sequenceNameForEntity(entity); if (!sequenceNames.containsObject(sequenceName)) { sequenceNames.addObject(sequenceName); // timc 2006-11-06 create result here so we can check for // enableIdentifierQuoting while building the statement H2Expression result = new H2Expression(entity); String attributeName = result.sqlStringForAttribute(priKeyAttribute); String tableName = result.sqlStringForSchemaObjectName(entity.externalName()); sql = "CREATE SEQUENCE " + sequenceName + " START WITH (SELECT MAX(" + attributeName + ") + 1 FROM " + tableName + ")"; results.addObject(createExpression(entity, sql)); sql = "ALTER TABLE " + tableName + " ALTER COLUMN " + attributeName + " SET DEFAULT nextval('" + sequenceName + "')"; results.addObject(createExpression(entity, sql)); } } return results; } @Override public NSArray<EOSQLExpression> statementsToConvertColumnType(String columnName, String tableName, EOSchemaSynchronization.ColumnTypes oldType, EOSchemaSynchronization.ColumnTypes newType, EOSchemaGenerationOptions options) { EOAttribute attr = new EOAttribute(); attr.setName(columnName); attr.setColumnName(columnName); attr.setExternalType(newType.name()); attr.setScale(newType.scale()); attr.setPrecision(newType.precision()); attr.setWidth(newType.width()); String columnTypeString = columnTypeStringForAttribute(attr); return new NSArray<EOSQLExpression>(_expressionForString("ALTER TABLE " + formatTableName(tableName) + " ALTER COLUMN " + formatColumnName(columnName) + " " + columnTypeString)); } @Override public NSArray<EOSQLExpression> statementsToDeleteColumnNamed(String columnName, String tableName, EOSchemaGenerationOptions options) { return new NSArray<EOSQLExpression>(_expressionForString("ALTER TABLE " + formatTableName(tableName) + " DROP COLUMN " + formatTableName(columnName))); } @Override public NSArray<EOSQLExpression> statementsToInsertColumnForAttribute(EOAttribute attribute, EOSchemaGenerationOptions options) { String clause = _columnCreationClauseForAttribute(attribute); return new NSArray<EOSQLExpression>(_expressionForString("ALTER TABLE " + formatTableName(attribute.entity().externalName()) + " ADD COLUMN " + clause)); } @Override public NSArray<EOSQLExpression> statementsToModifyColumnNullRule(String columnName, String tableName, boolean allowsNull, EOSchemaGenerationOptions options) { NSArray<EOSQLExpression> statements; if (allowsNull) { statements = new NSArray<EOSQLExpression>(_expressionForString("ALTER TABLE " + formatTableName(tableName) + " ALTER COLUMN " + formatColumnName(columnName) + " SET NULL")); } else { statements = new NSArray<EOSQLExpression>(_expressionForString("ALTER TABLE " + formatTableName(tableName) + " ALTER COLUMN " + formatColumnName(columnName) + " SET NOT NULL")); } return statements; } @Override public NSArray<EOSQLExpression> statementsToRenameColumnNamed(String columnName, String tableName, String newName, EOSchemaGenerationOptions options) { return new NSArray<EOSQLExpression>(_expressionForString("ALTER TABLE " + formatTableName(tableName) + " ALTER COLUMN " + formatColumnName(columnName) + " RENAME TO " + formatColumnName(newName))); } @Override public NSArray<EOSQLExpression> statementsToRenameTableNamed(String oldTableName, String newTableName, EOSchemaGenerationOptions options) { return new NSArray<EOSQLExpression>(_expressionForString("ALTER TABLE " + formatTableName(oldTableName) + " RENAME TO " + formatTableName(newTableName))); } @Override public boolean supportsSchemaSynchronization() { return true; } /** * <code>H2Expression</code> factory method. * * @param entity * the entity to which <code>H2Expression</code> is to * be rooted * @param statement * the SQL statement * @return a <code>H2Expression</code> rooted to <code>entity</code> */ private static H2Expression createExpression(EOEntity entity, String statement) { H2Expression result = new H2Expression(entity); result.setStatement(statement); return result; } } private static final String DRIVER_CLASS_NAME = "org.h2.Driver"; private static final String DRIVER_NAME = "H2"; /** * flag for whether jdbcInfo should be written out has been tested. */ private volatile boolean testedJdbcInfo; public _H2PlugIn(final JDBCAdaptor adaptor) { super(adaptor); } @Override public EOSchemaSynchronizationFactory createSchemaSynchronizationFactory() { return new H2SynchronizationFactory(adaptor()); } @Override public String databaseProductName() { return DRIVER_NAME; } @Override public String defaultDriverName() { return DRIVER_CLASS_NAME; } @Override public Class<? extends JDBCExpression> defaultExpressionClass() { return H2Expression.class; } /** * <p> * This is usually extracted from the the database using JDBC, but this is * really inconvenient for users who are trying to generate SQL at some. A * specific version of the data has been written into the property list of * the framework and this can be used as a hard-coded equivalent. * </p> * <p> * Provide system property <code>h2.updateJDBCInfo=true</code> to * cause H2JDBCInfo.plist to be written out to the platform temp dir. * </p> */ @Override public NSDictionary<String, Object> jdbcInfo() { // optionally write out a fresh copy of the H2JDBCInfo.plist file. if (!testedJdbcInfo) { testedJdbcInfo = true; String property = System.getProperty("h2.updateJDBCInfo"); if (NSPropertyListSerialization.booleanForString(property)) { NSLog.out.appendln("Updating H2JDBCInfo.plist enabled:" + property); try { String jdbcInfoContent = NSPropertyListSerialization.stringFromPropertyList(super.jdbcInfo()); File tmpDir = new File(System.getProperty("java.io.tmpdir")); File jdbcInfoFile = new File(tmpDir, "H2JDBCInfo.plist"); NSLog.out.appendln("Writing H2JDBCInfo.plist to " + tmpDir.getAbsolutePath()); FileOutputStream fos = new FileOutputStream(jdbcInfoFile); fos.write(jdbcInfoContent.getBytes()); fos.close(); } catch (Exception e) { throw new IllegalStateException("problem writing H2JDBCInfo.plist", e); } } } NSDictionary<String, Object> jdbcInfo; // have a look at the JDBC connection URL to see if the flag has been // set to // specify that the hard-coded jdbcInfo information should be used. if (shouldUseBundledJdbcInfo()) { if (NSLog.debugLoggingAllowedForLevel(NSLog.DebugLevelDetailed)) { NSLog.debug.appendln("Loading jdbcInfo from H2JDBCInfo.plist as opposed to using the JDBCPlugIn default implementation."); } InputStream jdbcInfoStream = NSBundle.bundleForClass(getClass()).inputStreamForResourcePath("H2JDBCInfo.plist"); if (jdbcInfoStream == null) { throw new IllegalStateException("Unable to find 'H2JDBCInfo.plist' in this plugin jar."); } try { jdbcInfo = (NSDictionary<String, Object>) NSPropertyListSerialization.propertyListFromData(new NSData(jdbcInfoStream, 2048), "US-ASCII"); } catch (IOException e) { throw new RuntimeException("Failed to load 'H2JDBCInfo.plist' from this plugin jar: " + e, e); } finally { try { jdbcInfoStream.close(); } catch (IOException e) {} } } else { jdbcInfo = super.jdbcInfo(); } return jdbcInfo; } @Override public String name() { return DRIVER_NAME; } /** * This method returns <code>true</code> by default unless the connection URL for the database has * <code>useBundledJdbcInfo=false</code> on it which indicates to the system * that the jdbcInfo which has been bundled into the plugin is not acceptable to * use and instead it should fetch a fresh copy from the database. * * @return <code>true</code> if bundled jdbcInfo should be used */ protected boolean shouldUseBundledJdbcInfo() { boolean shouldUseBundledJdbcInfo = true; String url = connectionURL(); if (url != null && url.toLowerCase().matches(".*(\\?|\\?.*&)useBundledJdbcInfo=(false|no)(\\&|$)".toLowerCase())) { shouldUseBundledJdbcInfo = false; } return shouldUseBundledJdbcInfo; } /** * Overrides the parent implementation to provide a more efficient mechanism * for generating primary keys, while generating the primary key support on * the fly. * * @param count * the batch size * @param entity * the entity requesting primary keys * @param channel * open JDBCChannel * @return NSArray of NSDictionary where each dictionary corresponds to a * unique primary key value */ @Override public NSArray<NSDictionary<String, Object>> newPrimaryKeys(int count, EOEntity entity, JDBCChannel channel) { if (isPrimaryKeyGenerationNotSupported(entity)) { return null; } EOAttribute attribute = entity.primaryKeyAttributes().lastObject(); String attrName = attribute.name(); boolean isIntType = "i".equals(attribute.valueType()); NSMutableArray<NSDictionary<String, Object>> results = new NSMutableArray<NSDictionary<String, Object>>(count); String sequenceName = _sequenceNameForEntity(entity); H2Expression expression = new H2Expression(entity); int keysPerBatch = 20; boolean succeeded = false; for (int tries = 0; !succeeded && tries < 2; tries++) { while (results.count() < count) { try { StringBuilder sql = new StringBuilder(); sql.append("SELECT "); for (int keyBatchNum = Math.min(keysPerBatch, count - results.count()) - 1; keyBatchNum >= 0; keyBatchNum--) { sql.append("NEXTVAL('" + sequenceName + "') AS KEY" + keyBatchNum); if (keyBatchNum > 0) { sql.append(", "); } } expression.setStatement(sql.toString()); channel.evaluateExpression(expression); try { NSDictionary<String, Object> row; while ((row = channel.fetchRow()) != null) { Enumeration pksEnum = row.allValues().objectEnumerator(); while (pksEnum.hasMoreElements()) { Number pkObj = (Number) pksEnum.nextElement(); Number pk; if (isIntType) { pk = Integer.valueOf(pkObj.intValue()); } else { pk = Long.valueOf(pkObj.longValue()); } results.addObject(new NSDictionary<String, Object>(pk, attrName)); } } } finally { channel.cancelFetch(); } succeeded = true; } catch (JDBCAdaptorException e) { // jw check if H2 has already a sequence with a different name String tableName = entity.externalName().toUpperCase(); String columnName = attribute.columnName().toUpperCase(); int dotIndex = tableName.indexOf("."); if (dotIndex == -1) { expression.setStatement("select SEQUENCE_NAME, COLUMN_DEFAULT from INFORMATION_SCHEMA.COLUMNS where UPPER(TABLE_NAME) = '" + tableName + "' and UPPER(COLUMN_NAME) = '" + columnName + "'"); } else { String schemaName = tableName.substring(0, dotIndex); String tableNameOnly = tableName.substring(dotIndex + 1); expression.setStatement("select SEQUENCE_NAME, COLUMN_DEFAULT from INFORMATION_SCHEMA.COLUMNS where UPPER(TABLE_NAME) = '" + tableNameOnly + "' and UPPER(COLUMN_NAME) = '" + columnName + "' and UPPER(TABLE_SCHEMA) = '" + schemaName + "'"); } channel.evaluateExpression(expression); NSDictionary<String, Object> row; try { row = channel.fetchRow(); if (row != null) { Object obj = row.objectForKey("SEQUENCE_NAME"); String h2SequenceName = obj == NSKeyValueCoding.NullValue ? null : (String) obj; if (h2SequenceName == null) { obj = row.objectForKey("COLUMN_DEFAULT"); String defaultValue = obj == NSKeyValueCoding.NullValue ? null : (String) obj; if (defaultValue != null) { final String NEXT_VAL = "NEXTVAL('"; int startPos = defaultValue.indexOf(NEXT_VAL); if (startPos != -1) { int endPos = defaultValue.indexOf("')"); h2SequenceName = defaultValue.substring(startPos + NEXT_VAL.length(), endPos); } else { final String NEXT_FOR = "NEXT VALUE FOR "; startPos = defaultValue.indexOf(NEXT_FOR); if (startPos != -1) { int dotPos = defaultValue.indexOf(".", startPos) + NEXT_FOR.length(); if (dotPos != -1) { startPos = dotPos; } h2SequenceName = defaultValue.substring(startPos + 1, defaultValue.length() - 1); } } } } if (h2SequenceName != null) { // store sequence name mapping as H2 does not yet support renaming of sequences setSequenceNameOverride(sequenceName, h2SequenceName); sequenceName = h2SequenceName; continue; } } } catch (IllegalArgumentException iae) { // failed to retrieve alternative sequence name } finally { channel.cancelFetch(); } //timc 2006-11-06 Check if sequence name contains schema name dotIndex = sequenceName.indexOf("."); if (dotIndex == -1) { expression.setStatement("select count(*) as COUNT from INFORMATION_SCHEMA.SEQUENCES where SEQUENCE_NAME = '" + sequenceName.toUpperCase() + "'"); } else { String schemaName = sequenceName.substring(0, dotIndex); String sequenceNameOnly = sequenceName.toLowerCase().substring(dotIndex + 1); expression .setStatement("select count(*) as COUNT from INFORMATION_SCHEMA.SEQUENCES where SEQUENCE_SCHEMA = '" + schemaName.toUpperCase() + "' AND SEQUENCE_NAME = '" + sequenceNameOnly.toUpperCase() + "'"); } channel.evaluateExpression(expression); try { row = channel.fetchRow(); } finally { channel.cancelFetch(); } // timc 2006-11-06 row.objectForKey("COUNT") returns BigDecimal not Long Number numCount = (Number) row.objectForKey("COUNT"); if (numCount != null && numCount.longValue() == 0L) { EOSchemaSynchronizationFactory f = createSchemaSynchronizationFactory(); NSArray<EOSQLExpression> statements = f.primaryKeySupportStatementsForEntityGroup(new NSArray<EOEntity>(entity)); int stmCount = statements.count(); for (int i = 0; i < stmCount; i++) { channel.evaluateExpression(statements.objectAtIndex(i)); } } else if (numCount == null) { throw new IllegalStateException("Couldn't call sequence " + sequenceName + " and couldn't get sequence information from pg_class: " + e); } else { throw new IllegalStateException("Caught exception, but sequence did already exist: " + e); } } } } if (results.count() != count) { throw new IllegalStateException("Unable to generate primary keys from the sequence for " + entity + "."); } return results; } /** * Checks whether primary key generation can be supported for <code>entity</code> * * @param entity the entity to be checked * @return yes/no */ private static boolean isPrimaryKeyGenerationNotSupported(EOEntity entity) { return entity.primaryKeyAttributes().count() > 1 || entity.primaryKeyAttributes().lastObject().adaptorValueType() != EOAttribute.AdaptorNumberType; } }