package com.webobjects.jdbcadaptor;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.webobjects.eoaccess.EOAttribute;
import com.webobjects.eoaccess.EOEntity;
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.NSForwardException;
import com.webobjects.foundation.NSLog;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSPropertyListSerialization;
/**
* WO runtime plugin with support for Postgresql.
*
* @author ak
* @author giorgio_v
*/
public class PostgresqlPlugIn extends JDBCPlugIn {
private static final String QUERY_STRING_USE_BUNDLED_JDBC_INFO = "useBundledJdbcInfo";
static {
setPlugInNameForSubprotocol(PostgresqlPlugIn.class.getName(), "postgresql");
}
/**
* Designated constructor.
*/
public PostgresqlPlugIn(JDBCAdaptor adaptor) {
super(adaptor);
}
@Override
public String defaultDriverName() {
return "org.postgresql.Driver";
}
@Override
public String databaseProductName() {
return "Postgresql";
}
/**
* WebObjects 5.4's version of JDBCAdaptor will use this
* in order to assemble the name of the prototype to use when
* it loads models.
*
* @return the name of the plugin
*/
@Override
public String name() {
return "Postgresql";
}
/**
* This method returns <code>true</code> if the connection URL for the
* database has a special flag 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 jdbcInfo should be used
*/
protected boolean shouldUseBundledJdbcInfo() {
boolean shouldUseBundledJdbcInfo = false;
String url = connectionURL();
if (url != null) {
Matcher matcher = Pattern.compile(PostgresqlPlugIn.QUERY_STRING_USE_BUNDLED_JDBC_INFO.toLowerCase() + "=(true|yes)").matcher(url.toLowerCase());
shouldUseBundledJdbcInfo = matcher.find();
}
return shouldUseBundledJdbcInfo;
}
/**
* 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 jdbcInfo dictionary
*/
@Override
public NSDictionary<String, Object> 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<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 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<String, Object>) NSPropertyListSerialization.propertyListFromData(new NSData(jdbcInfoStream, 2048), "US-ASCII");
}
catch (IOException e) {
throw new RuntimeException("Failed to load 'JDBCInfo.plist' from this plugin jar.", e);
}
finally {
try {
jdbcInfoStream.close();
}
catch (IOException e) {
// ignore
}
}
} else {
jdbcInfo = super.jdbcInfo();
}
return jdbcInfo;
}
/**
* Returns a "pure java" synchronization factory.
* Useful for testing purposes.
*/
@Override
public EOSynchronizationFactory createSynchronizationFactory() {
try {
return new PostgresqlSynchronizationFactory(adaptor());
}
catch (Exception e) {
throw new NSForwardException(e, "Couldn't create synchronization factory");
}
}
/**
* Expression class to create. We have custom code, so we need our own class.
*/
@Override
public Class<? extends JDBCExpression> defaultExpressionClass() {
return PostgresqlExpression.class;
}
/**
* 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);
PostgresqlExpression expression = new PostgresqlExpression(entity);
// MS: The original implementation of this did something like select setval('seq', nextval('seq') + count)
// which apparently is not an atomic operation, which causes terrible problems under load with multiple
// instances. The new implementation does batch requests for keys.
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<>(pk, attrName));
}
}
}
finally {
channel.cancelFetch();
}
succeeded = true;
}
catch (JDBCAdaptorException ex) {
//timc 2006-11-06 Check if sequence name contains schema name
int dotIndex = sequenceName.indexOf(".");
if (dotIndex == -1) {
expression.setStatement("select count(*) from pg_class where relname = '" + sequenceName.toLowerCase() + "' and relkind = 'S'");
}
else {
String schemaName = sequenceName.substring(0, dotIndex);
String sequenceNameOnly = sequenceName.toLowerCase().substring(dotIndex + 1);
expression.setStatement("select count(c.*) from pg_catalog.pg_class c, pg_catalog.pg_namespace n where c.relnamespace=n.oid AND c.relkind = 'S' AND c.relname='" + sequenceNameOnly + "' AND n.nspname='" + schemaName + "'");
}
channel.evaluateExpression(expression);
NSDictionary<String, Object> row;
try {
row = channel.fetchRow();
}
finally {
channel.cancelFetch();
}
// timc 2006-11-06 row.objectForKey("COUNT") returns BigDecimal not Long
//if( Long.valueOf( 0 ).equals( row.objectForKey( "COUNT" ) ) ) {
Number numCount = (Number) row.objectForKey("COUNT");
if (numCount != null && numCount.longValue() == 0L) {
EOSynchronizationFactory f = createSynchronizationFactory();
NSArray<EOSQLExpression> statements = f.primaryKeySupportStatementsForEntityGroup(new NSArray<>(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: " + ex);
}
else {
throw new IllegalStateException("Caught exception, but sequence did already exist: " + ex);
}
}
}
}
if (results.count() != count) {
throw new IllegalStateException("Unable to generate primary keys from the sequence for " + entity + ".");
}
return results;
}
/**
* Utility method that returns the name of the sequence associated
* with <code>entity</code>
*
* @param entity the entity
* @return the name of the sequence
*/
protected static String _sequenceNameForEntity(EOEntity entity) {
/* timc 2006-11-06
* This used to say ... + "_SEQ";
* _SEQ would get converted to _seq because postgresql converts all unquoted identifiers to lower case.
* In the future we may use enableIdentifierQuoting for sequence names so we need to set the correct case here in the first place
*/
return entity.primaryKeyRootName() + "_seq";
}
/**
* Checks whether primary key generation can be supported for <code>entity</code>
*
* @param entity the entity to be checked
* @return yes/no
*/
private boolean isPrimaryKeyGenerationNotSupported(EOEntity entity) {
return entity.primaryKeyAttributes().count() > 1 || entity.primaryKeyAttributes().lastObject().adaptorValueType() != EOAttribute.AdaptorNumberType;
}
}