package com.webobjects.jdbcadaptor; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.math.BigDecimal; import java.sql.Clob; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import com.webobjects.eoaccess.EOAdaptor; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EORelationship; import com.webobjects.eoaccess.EOSQLExpression; import com.webobjects.eoaccess.EOSynchronizationFactory; 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.NSPropertyListSerialization; import com.webobjects.foundation.NSTimestamp; import com.webobjects.foundation.NSTimestampFormatter; public class _DerbyPlugIn extends JDBCPlugIn { static final boolean USE_NAMED_CONSTRAINTS = true; protected static String quoteTableName(String s) { if (s == null) return null; int i = s.lastIndexOf(46); if (i == -1) return "\"" + s + "\""; else return "\"" + s.substring(0, i) + "\".\"" + s.substring(i + 1, s.length()) + "\""; } public static class DerbyExpression extends JDBCExpression { // more to come public DerbyExpression(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 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 obj, final EOAttribute eoattribute) { String value; if (obj instanceof NSData) { value = sqlStringForData((NSData) obj); } else if ((obj instanceof NSTimestamp) && isTimestampAttribute(eoattribute)) { value = "'" + TIMESTAMP_FORMATTER.format(obj) + "'"; } else if ((obj instanceof NSTimestamp) && isDateAttribute(eoattribute)) { value = "'" + DATE_FORMATTER.format(obj) + "'"; } else if (obj instanceof String) { value = formatStringValue((String) obj); } else if (obj instanceof Number) { if (obj instanceof BigDecimal) { value = formatBigDecimal((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 { value = convertedObj.toString(); } } else { value = convertedObj.toString(); } } } else if (obj instanceof Boolean) { // GN: when booleans are stored as strings in the db, we need // the values quoted if (enableBooleanQuoting()) { value = "'" + ((Boolean) obj).toString() + "'"; } 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 NSData || adaptorValue instanceof NSTimestamp || adaptorValue instanceof String || adaptorValue instanceof Number || adaptorValue instanceof Boolean) { value = formatValueForAttribute(adaptorValue, eoattribute); } else { NSLog.err.appendln(getClass().getName() + ": Can't convert: " + obj + ":" + obj.getClass() + " -> " + adaptorValue + ":" + adaptorValue.getClass()); value = obj.toString(); } } catch (Exception ex) { NSLog.err.appendln(getClass().getName() + ": Exception while converting " + obj.getClass().getName()); NSLog.err.appendln(ex); value = obj.toString(); } } return value; } /** * Helper to check for timestamp columns that have a "D" value type. * * @param eoattribute * @return <code>true</code> if date attribute */ private boolean isDateAttribute(final EOAttribute eoattribute) { return "D".equals(eoattribute.valueType()); } /** * Helper to check for timestamp columns that have a "T" value type. * * @param eoattribute * @return <code>true</code> if timestamp attribute */ private boolean isTimestampAttribute(final EOAttribute eoattribute) { return "T".equals(eoattribute.valueType()); } } public static class DerbySynchronizationFactory extends EOSynchronizationFactory { public DerbySynchronizationFactory(final EOAdaptor adaptor) { super(adaptor); } @Override public String _columnCreationClauseForAttribute(EOAttribute attribute) { return addCreateClauseForAttribute(attribute).toString(); } public StringBuffer addCreateClauseForAttribute(EOAttribute attribute) { EOSQLExpression expression = _expressionForEntity(attribute.entity()); expression.addCreateClauseForAttribute(attribute); return new StringBuffer(expression.listString()); } @Override public NSArray<EOSQLExpression> _statementsToDropPrimaryKeyConstraintsOnTableNamed(final String tableName) { return new NSArray<>(_expressionForString("alter table " + formatTableName(tableName) + " drop primary key")); } @Override public NSArray<EOSQLExpression> dropPrimaryKeySupportStatementsForEntityGroups(final NSArray entityGroups) { String pkTable = ((JDBCAdaptor) adaptor()).plugIn().primaryKeyTableName(); return new NSArray<>(_expressionForString("drop table " + formatTableName(pkTable))); } @Override public NSArray<EOSQLExpression> dropTableStatementsForEntityGroup(final NSArray<EOEntity> entityGroup) { return new NSArray<>(_expressionForString("drop table " + formatTableName(entityGroup.objectAtIndex(0).externalName()))); } @Override public NSArray<EOSQLExpression> statementsToModifyColumnNullRule(String columnName, String tableName, boolean allowsNull, NSDictionary nsdictionary) { NSArray<EOSQLExpression> statements; if (allowsNull) { statements = new NSArray<>(_expressionForString("alter table " + formatTableName(tableName) + " alter column " + formatColumnName(columnName) + " null")); } else { statements = new NSArray<>(_expressionForString("alter table " + formatTableName(tableName) + " alter column " + formatColumnName(columnName) + " not null")); } return statements; } 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.isToMany() && isPrimaryKeyAttributes(relationship.destinationEntity(), relationship.destinationAttributes())) { StringBuilder sql = new StringBuilder(); String tableName = relationship.entity().externalName(); sql.append("ALTER TABLE "); sql.append(quoteTableName(tableName.toUpperCase())); sql.append(" ADD"); StringBuilder constraint = new StringBuilder(" CONSTRAINT \"FOREIGN_KEY_"); constraint.append(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(", "); fkSql.append("\""); String columnName = attributes.objectAtIndex(i).columnName(); fkSql.append(columnName.toUpperCase()); constraint.append(columnName); fkSql.append("\""); } fkSql.append(") REFERENCES "); constraint.append('_'); String referencedExternalName = relationship.destinationEntity().externalName(); fkSql.append(quoteTableName(referencedExternalName.toUpperCase())); constraint.append(referencedExternalName); fkSql.append(" ("); attributes = relationship.destinationAttributes(); for (int i = 0; i < attributes.count(); i++) { constraint.append('_'); if (i != 0) fkSql.append(", "); fkSql.append("\""); String referencedColumnName = attributes.objectAtIndex(i).columnName(); fkSql.append(referencedColumnName.toUpperCase()); constraint.append(referencedColumnName); fkSql.append("\""); } // MS: did i write this code? sorry about that everything. this is crazy. constraint.append("\""); // New with version 10.11, by default constraint are now immediate fkSql.append(") DEFERRABLE INITIALLY DEFERRED"); if (USE_NAMED_CONSTRAINTS) sql.append(constraint); sql.append(fkSql); return new NSArray<>(_expressionForString(sql.toString())); } return NSArray.EmptyArray; } @Override public NSArray<EOSQLExpression> primaryKeySupportStatementsForEntityGroups(final NSArray entityGroups) { String pkTable = ((JDBCAdaptor) adaptor()).plugIn().primaryKeyTableName(); return new NSArray<>(_expressionForString("create table " + formatTableName(pkTable) + " (name char(40) primary key, pk INT)")); } @Override public NSArray<EOSQLExpression> statementsToRenameColumnNamed(String columnName, String tableName, String newName, NSDictionary nsdictionary) { return new NSArray<>(_expressionForString("rename column " + formatTableName(tableName) + "." + formatColumnName(columnName) + " to " + formatColumnName(newName))); } @Override public NSArray<EOSQLExpression> statementsToInsertColumnForAttribute(final EOAttribute attribute, final NSDictionary options) { String clause = _columnCreationClauseForAttribute(attribute); return new NSArray<>(_expressionForString("alter table " + formatTableName(attribute.entity().externalName()) + " add column " + clause)); } @Override public NSArray<EOSQLExpression> statementsToRenameTableNamed(String tableName, String newName, NSDictionary options) { return new NSArray<>(_expressionForString("rename table " + formatTableName(tableName) + " to " + formatTableName(newName))); } @Override public boolean supportsSchemaSynchronization() { return true; } } private static final String DRIVER_CLASS_NAME = "org.apache.derby.jdbc.EmbeddedDriver"; private static final String DRIVER_NAME = "Derby"; /** * formatter to use when handling date columns */ private static final NSTimestampFormatter DATE_FORMATTER = new NSTimestampFormatter("%Y-%m-%d"); /** * formatter to use when handling timestamps */ private static final NSTimestampFormatter TIMESTAMP_FORMATTER = new NSTimestampFormatter("%Y-%m-%d %H:%M:%S.%F"); /** * Method to get the string value from a BigDecimals from. */ private static Method _bigDecimalToString = null; public _DerbyPlugIn(final JDBCAdaptor adaptor) { super(adaptor); } @Override public EOSynchronizationFactory createSynchronizationFactory() { return new DerbySynchronizationFactory(adaptor()); } @Override public String databaseProductName() { return DRIVER_NAME; } @Override public String defaultDriverName() { return DRIVER_CLASS_NAME; } @Override public Class<? extends JDBCExpression> defaultExpressionClass() { return DerbyExpression.class; } @Override public Object fetchCLOB(ResultSet resultSet, int column, EOAttribute attribute, boolean materialize) throws SQLException { Clob clob = resultSet.getClob(column); if (clob == null) { return null; } if (!materialize) { return clob; } return clob.getSubString(1L, (int)clob.length()); } /** * 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. * * @return JDBC info */ @Override public NSDictionary jdbcInfo() { // you can swap this code out to write the property list out in order // to get a fresh copy of the JDBCInfo.plist. // try { // String jdbcInfoS = // NSPropertyListSerialization.stringFromPropertyList(super.jdbcInfo()); // FileOutputStream fos = new FileOutputStream("/tmp/JDBCInfo.plist"); // fos.write(jdbcInfoS.getBytes()); // fos.close(); // } // catch(Exception e) { // throw new IllegalStateException("problem writing JDBCInfo.plist",e); // } NSDictionary 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 JDBCInfo.plist as opposed to using the JDBCPlugIn default implementation."); } InputStream jdbcInfoStream = NSBundle.bundleForClass(getClass()).inputStreamForResourcePath("JDBCInfo.plist"); if (jdbcInfoStream == null) { throw new IllegalStateException("Unable to find 'JDBCInfo.plist' in this plugin jar."); } try { jdbcInfo = (NSDictionary) NSPropertyListSerialization.propertyListFromData(new NSData(jdbcInfoStream, 2048), "US-ASCII"); } catch (IOException e) { throw new RuntimeException("Failed to load 'JDBCInfo.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> if the connection URL for the database has * <code>useBundledJdbcInfo=true</code> on it which indicates to the system * that the jdbcInfo which has been bundled into the plugin is acceptable to * use in place of actually going to the database and getting it. * * @return <code>true</code> if bundled JDBC info should be used */ protected boolean shouldUseBundledJdbcInfo() { boolean shouldUseBundledJdbcInfo = false; String url = connectionURL(); if (url != null) { shouldUseBundledJdbcInfo = url.toLowerCase().matches(".*(\\?|\\?.*&)useBundledJdbcInfo=(true|yes)(\\&|$)".toLowerCase()); } return shouldUseBundledJdbcInfo; } }