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;
}
}